Skip to content

Evaluate broker routing SpEL expressions against the original event #1896

Closed

Description

@rolandbeisel

Problem

Broker routing SpEL expressions are currently evaluated against the mapped transport payload instead of the original domain event.

EventExternalizerSupport first determines the unresolved RoutingTarget from the original event and then applies the configured mapping:

var target = configuration.determineTarget(event);
var mapped = configuration.map(event);

return externalize(mapped, target);

The broker transport receives only mapped and target. It therefore uses the mapped payload as the root object when it resolves dynamic target and key expressions. Kafka currently does the equivalent of:

var routing = BrokerRouting.of(target, context);

builder
		.setHeaderIfAbsent(KafkaHeaders.KEY, routing.getKey(payload))
		.setHeaderIfAbsent(KafkaHeaders.TOPIC, routing.getTarget(payload));

At this point, payload is the result of configuration.map(event).

The same pattern is used by the AMQP, JMS and Spring Messaging transports. This behavior is independent of the selected externalization mode and also occurs in regular module-listener mode.

Example

@Externalized("#{'orders-' + getTenant()}::#{getOrderId()}")
record OrderCompleted(String tenant, UUID orderId) {

	public String getTenant() {
		return tenant;
	}

	public UUID getOrderId() {
		return orderId;
	}
}

record OrderCompletedMessage(byte[] data) {}

@Bean
EventExternalizationConfiguration externalization() {
	return EventExternalizationConfiguration.externalizing()
			.select(EventExternalizationConfiguration.annotatedAsExternalized())
			.mapping(OrderCompleted.class, this::toMessage)
			.build();
}

The expressions declared on OrderCompleted refer to getTenant() and getOrderId(). Externalization fails because those expressions are evaluated against OrderCompletedMessage, which does not expose either method.

The failure occurs with direct module-listener externalization as well as with JobRunr or Namastack outbox delivery.

Expected behavior

Target and key expressions declared for a domain event should use that original domain event as their evaluation root. Mapping should control the payload sent to the broker, but should not change the object used to resolve the event's routing metadata.

For the example above, the expected result is:

  • target: orders-<tenant> resolved from OrderCompleted;
  • key: the original event's orderId;
  • broker payload: OrderCompletedMessage.

Static routing values and explicitly supplied message headers should retain their existing behavior.

Design consideration

The transport currently receives only the mapped payload and the unresolved RoutingTarget, so it cannot evaluate an expression against the original event. The externalization pipeline needs to preserve the original event as routing context or resolve the routing information before handing the mapped payload to the transport.

The solution should apply consistently to Kafka, AMQP, JMS and Spring Messaging and should not depend on thread-local state.

Acceptance criteria

  • Dynamic target expressions are evaluated against the original event.
  • Dynamic key expressions are evaluated against the original event.
  • The mapped payload is still sent or serialized by the broker transport.
  • Behavior is identical in module-listener, JobRunr outbox and Namastack outbox modes.
  • Kafka, AMQP, JMS and Spring Messaging use the same routing semantics.
  • Mapping to a type that does not expose properties referenced by the event's routing expressions succeeds.
  • Existing static routing and message-header behavior remains unchanged.

Activity

  1. self-assigned this
    on Sep 23, 2026
  2. added this to the 2.2.0-RC1 milestone on Sep 23, 2026
  3. added a commit that references this issue on Oct 1, 2026
  4. added a commit that references this issue on Oct 1, 2026
  5. added a commit that references this issue on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions