Implementing Event-Driven Architecture

1. Designing Domain Events

FieldDescription
eventIdUUID, unique
eventTypePast tense: OrderPlaced, PaymentCaptured
aggregateIdEntity the event refers to
aggregateTypee.g. Order
eventVersionSchema version (1, 2, ...)
occurredAtISO-8601 UTC timestamp
causationId / correlationIdTrace causality
payloadImmutable business facts

Example: CloudEvents-style domain event

{
  "specversion": "1.0",
  "id": "ev_01HXYZ",
  "type": "shop.order.placed.v1",
  "source": "/orders",
  "subject": "ord_123",
  "time": "2026-05-15T12:00:00Z",
  "datacontenttype": "application/json",
  "data": { "orderId": "ord_123", "customerId": "cus_9", "total": 49.95 }
}

2. Implementing Event Sourcing

ConceptDetail
StateDerived from full event history
Append-only logEvents immutable; never edited
ReplayRebuild state by replaying
SnapshotPeriodic state save to speed replay
ToolsEventStoreDB, Axon, Marten, Kafka + KTable

3. Using Event Streams

ConceptDetail
StreamOrdered, replayable sequence
TopicNamed stream (Kafka)
OffsetPer-consumer position
CompactedKeep latest value per key (changelog)
Stream processingKafka Streams, Flink, ksqlDB

4. Implementing Event Versioning

StrategyHow
Add fields with defaultBackward compatible
New event typeOrderPlaced.v2 alongside v1
UpcasterTransform old events into new shape on read
Schema registryConfluent / Apicurio enforces compat

5. Handling Event Ordering

TechniqueDetail
Partition by aggregate IDPer-aggregate FIFO
Single-threaded consumerPer partition
Sequence numberDetect gaps/out-of-order
Vector clocksTrack causal relationships

6. Implementing Event Replay

Use CaseApproach
New projectionReplay from offset 0
Bug fixReplay from known offset
Time travelReplay to specific timestamp
CaveatSide-effects (emails, payments) must be guarded

7. Using Event Schemas

FormatStrengths
JSON SchemaHuman-readable, ubiquitous
AvroCompact, schema evolution rules
ProtobufCompact + RPC ecosystem
CloudEventsStandard envelope
Schema RegistryConfluent, Apicurio, AWS Glue

8. Implementing Event Handlers

ConcernPractice
IdempotencyTrack processed eventId
Side-effect isolationOutbox or transactional inbox
RetryBackoff + DLQ
ConcurrencyPer-partition single-threaded

9. Handling Event Consistency

PatternDetail
Transactional OutboxWrite event + state in one DB tx; relay later
Inbox PatternPersist event before processing for dedup
CDCDebezium streams DB log → topic
AvoidDual-write to DB + broker without outbox

Example: Outbox table

CREATE TABLE outbox (
  id            BIGSERIAL PRIMARY KEY,
  aggregate_id  TEXT NOT NULL,
  event_type    TEXT NOT NULL,
  payload       JSONB NOT NULL,
  occurred_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  published_at  TIMESTAMPTZ
);
CREATE INDEX ON outbox (published_at) WHERE published_at IS NULL;

10. Using Event Correlation

IDPurpose
eventIdThis event's identity
causationIdThe event/command that caused this
correlationIdWorkflow / business transaction
traceId / spanIdObservability (W3C)

11. Implementing Event Notification vs Event-Carried State Transfer

Event Notification

  • Thin event: just IDs
  • Consumer calls source service for details
  • + Small payload, source-of-truth
  • − Sync coupling, N+1 calls

Event-Carried State Transfer

  • Event carries full state snapshot
  • Consumer self-sufficient
  • + Decoupled, fast
  • − Larger events, stale data risk

12. Managing Event Store

FeatureRequirement
Append-only writesOptimistic concurrency on aggregate version
Per-aggregate streamRead all events for one entity
Subscribe by categoryStream by event type
SnapshotsSkip ahead in long streams
ChoicesEventStoreDB, Marten on Postgres, Axon Server