Implementing Event-Driven Architecture
1. Designing Domain Events
| Field | Description |
|---|---|
| eventId | UUID, unique |
| eventType | Past tense: OrderPlaced, PaymentCaptured |
| aggregateId | Entity the event refers to |
| aggregateType | e.g. Order |
| eventVersion | Schema version (1, 2, ...) |
| occurredAt | ISO-8601 UTC timestamp |
| causationId / correlationId | Trace causality |
| payload | Immutable 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
| Concept | Detail |
|---|---|
| State | Derived from full event history |
| Append-only log | Events immutable; never edited |
| Replay | Rebuild state by replaying |
| Snapshot | Periodic state save to speed replay |
| Tools | EventStoreDB, Axon, Marten, Kafka + KTable |
3. Using Event Streams
| Concept | Detail |
|---|---|
| Stream | Ordered, replayable sequence |
| Topic | Named stream (Kafka) |
| Offset | Per-consumer position |
| Compacted | Keep latest value per key (changelog) |
| Stream processing | Kafka Streams, Flink, ksqlDB |
4. Implementing Event Versioning
| Strategy | How |
|---|---|
| Add fields with default | Backward compatible |
| New event type | OrderPlaced.v2 alongside v1 |
| Upcaster | Transform old events into new shape on read |
| Schema registry | Confluent / Apicurio enforces compat |
5. Handling Event Ordering
| Technique | Detail |
|---|---|
| Partition by aggregate ID | Per-aggregate FIFO |
| Single-threaded consumer | Per partition |
| Sequence number | Detect gaps/out-of-order |
| Vector clocks | Track causal relationships |
6. Implementing Event Replay
| Use Case | Approach |
|---|---|
| New projection | Replay from offset 0 |
| Bug fix | Replay from known offset |
| Time travel | Replay to specific timestamp |
| Caveat | Side-effects (emails, payments) must be guarded |
7. Using Event Schemas
| Format | Strengths |
|---|---|
| JSON Schema | Human-readable, ubiquitous |
| Avro | Compact, schema evolution rules |
| Protobuf | Compact + RPC ecosystem |
| CloudEvents | Standard envelope |
| Schema Registry | Confluent, Apicurio, AWS Glue |
8. Implementing Event Handlers
| Concern | Practice |
|---|---|
| Idempotency | Track processed eventId |
| Side-effect isolation | Outbox or transactional inbox |
| Retry | Backoff + DLQ |
| Concurrency | Per-partition single-threaded |
9. Handling Event Consistency
| Pattern | Detail |
|---|---|
| Transactional Outbox | Write event + state in one DB tx; relay later |
| Inbox Pattern | Persist event before processing for dedup |
| CDC | Debezium streams DB log → topic |
| Avoid | Dual-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
| ID | Purpose |
|---|---|
| eventId | This event's identity |
| causationId | The event/command that caused this |
| correlationId | Workflow / business transaction |
| traceId / spanId | Observability (W3C) |
11. Implementing Event Notification vs Event-Carried State Transfer
12. Managing Event Store
| Feature | Requirement |
|---|---|
| Append-only writes | Optimistic concurrency on aggregate version |
| Per-aggregate stream | Read all events for one entity |
| Subscribe by category | Stream by event type |
| Snapshots | Skip ahead in long streams |
| Choices | EventStoreDB, Marten on Postgres, Axon Server |