Evaluate broker routing SpEL expressions against the original event

Open
#1,896 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
java, kafka, spring-boot

Research direction

Start by tracing EventExternalizerSupport from determineTarget(event) and map(event) into the Kafka, AMQP, JMS and Spring Messaging transports, focusing on how RoutingTarget expressions receive their evaluation root. Verify the behavior across module-listener, JobRunr outbox and Namastack outbox modes; done means dynamic target and key expressions use the original event while the mapped payload and existing static/header behavior remain unchanged.

Written by the indexing model from the issue text.

Description

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.
Dominant language
Java
Stars
1.2k
Forks
222
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from spring-projects/spring-modulith

All issues in spring-projects/spring-modulith

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.