Skip to content

Store the original domain event in the Namastack outbox #1895

Description

@rolandbeisel

Problem

NamastackOutboxEventRecorder applies the configured externalization mapping before it schedules an event in the outbox:

var target = configuration.determineTarget(payload);
var mapped = configuration.map(payload);
var routing = BrokerRouting.of(target, context);
var key = routing.getKey(mapped);

scheduleToOutbox(mapped, key);

This makes the Namastack outbox behave differently from the regular module-listener externalization flow. The outbox contains the mapped transport payload instead of the original domain event.

This causes several related problems:

  • The mapped payload has to be serializable by the outbox serializer. A mapping to an Avro-generated type, for example, can fail although the original domain event is Jackson-friendly.
  • Mapping and serialization failures occur while the business transaction is recorded instead of during outbox delivery. They therefore cannot be handled and reported as outbox delivery failures, such as through failure_reason.
  • The outbox handler receives an already mapped object and passes it to the externalizer. Selection and mapping are then applied to that object again, which can skip the event or attempt to map it twice.
  • The key stored with the outbox record is evaluated against the mapped payload rather than the original event.

This was originally reported in namastack/namastack-outbox#466.

Example

@Externalized("orders::#{getOrderId()}")
record OrderCompleted(UUID orderId) {

	public UUID getOrderId() {
		return orderId;
	}
}

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

Publishing OrderCompleted currently causes toAvro(…) to be invoked by the recorder. The resulting Avro object is serialized into the Namastack outbox and later handed back to the externalizer.

Expected behavior

The recorder should:

  1. determine whether the original event is externalized;
  2. determine the outbox record key from the original event;
  3. schedule the original event in the Namastack outbox without applying the externalization mapping.

When Namastack delivers the record, the existing outbox handler should pass the restored domain event to the externalizer. The normal externalization pipeline can then select, map and publish it exactly once.

This gives module-listener and Namastack outbox mode the same mapping lifecycle while keeping mapping and transport serialization failures within outbox processing.

Suggested change

Change NamastackOutboxEventRecorder to evaluate the outbox key against and schedule the original payload:

var target = configuration.determineTarget(payload);
var routing = BrokerRouting.of(target, context);
var key = routing.getKey(payload);

scheduleToOutbox(payload, key);

No change to the common externalizer or broker transports is required.

Acceptance criteria

  • The Namastack outbox stores the original domain event.
  • The externalization mapper is not invoked while the outbox record is created.
  • The outbox record key is evaluated against the original event.
  • The mapper is invoked during delivery by the outbox handler.
  • The mapped payload is sent to the configured broker.
  • Existing module-listener behavior remains unchanged.

Activity

  1. added 2 commits that reference this issue on Sep 20, 2026
  2. self-assigned this
    on Sep 23, 2026
  3. added this to the 2.2.0-RC1 milestone on Sep 23, 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