event-driven-architecture
Use this skill when designing event-driven systems, implementing event sourcing, applying CQRS patterns, selecting message brokers, or reasoning about eventual consistency. Triggers on tasks involving Kafka, RabbitMQ, event stores, command-query separation, domain events, sagas, compensating transactions, idempotency, message ordering, and any architecture where components communicate through asynchronous events rather than direct synchronous calls.
engineering event-sourcingcqrsmessage-brokerseventual-consistencyarchitecturedistributed-systemsWhat is event-driven-architecture?
Use this skill when designing event-driven systems, implementing event sourcing, applying CQRS patterns, selecting message brokers, or reasoning about eventual consistency. Triggers on tasks involving Kafka, RabbitMQ, event stores, command-query separation, domain events, sagas, compensating transactions, idempotency, message ordering, and any architecture where components communicate through asynchronous events rather than direct synchronous calls.
event-driven-architecture
event-driven-architecture is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Designing event-driven systems, implementing event sourcing, applying CQRS patterns, selecting message brokers, or reasoning about eventual consistency.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill event-driven-architecture- The event-driven-architecture skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A comprehensive guide to building systems where components communicate through events rather than direct calls. Event-driven architecture (EDA) decouples producers from consumers, enabling independent scaling, temporal decoupling, and resilience to downstream failures. This skill covers four core pillars: event sourcing (storing state as a sequence of events), CQRS (separating read and write models), message brokers (the transport layer), and eventual consistency (the consistency model that makes it all work). Agents use this skill to design, implement, and troubleshoot event-driven systems at any scale.
Tags
event-sourcing cqrs message-brokers eventual-consistency architecture distributed-systems
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair event-driven-architecture with these complementary skills:
Frequently Asked Questions
What is event-driven-architecture?
Use this skill when designing event-driven systems, implementing event sourcing, applying CQRS patterns, selecting message brokers, or reasoning about eventual consistency. Triggers on tasks involving Kafka, RabbitMQ, event stores, command-query separation, domain events, sagas, compensating transactions, idempotency, message ordering, and any architecture where components communicate through asynchronous events rather than direct synchronous calls.
How do I install event-driven-architecture?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill event-driven-architecture in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support event-driven-architecture?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Event-Driven Architecture
A comprehensive guide to building systems where components communicate through events rather than direct calls. Event-driven architecture (EDA) decouples producers from consumers, enabling independent scaling, temporal decoupling, and resilience to downstream failures. This skill covers four core pillars: event sourcing (storing state as a sequence of events), CQRS (separating read and write models), message brokers (the transport layer), and eventual consistency (the consistency model that makes it all work). Agents use this skill to design, implement, and troubleshoot event-driven systems at any scale.
When to use this skill
Trigger this skill when the user:
- Wants to implement event sourcing for an aggregate or service
- Needs to separate read and write models using CQRS
- Is choosing between Kafka, RabbitMQ, NATS, or other message brokers
- Asks about eventual consistency, compensation, or saga patterns
- Wants to design an event schema or event versioning strategy
- Needs to handle idempotency in event consumers
- Is debugging issues with message ordering, duplicate delivery, or consumer lag
- Asks about domain events, integration events, or event-carried state transfer
Do NOT trigger this skill for:
- Synchronous REST API design without an event component (use api-design)
- General system design questions about load balancers, caches, or CDNs (use system-design)
Key principles
Events are facts, not requests - An event records something that already happened (OrderPlaced, PaymentReceived). It is immutable. Commands request something to happen (PlaceOrder). Never conflate the two. Events use past tense; commands use imperative.
Design for at-least-once delivery - No message broker guarantees exactly-once delivery in all failure scenarios. Design every consumer to be idempotent. Use deduplication keys (event ID + consumer ID) or make operations naturally idempotent (SET over INCREMENT).
Own your events, share your contracts - The producing service owns the event schema. Consumers must not dictate what goes in an event. Publish a versioned schema contract (Avro, Protobuf, or JSON Schema) so consumers can evolve independently.
Separate the write model from the read model - CQRS lets you optimize writes for consistency and reads for query performance independently. The write side validates business rules; the read side denormalizes for fast lookups. They connect through events.
Embrace eventual consistency, but bound it - Eventual consistency is not "maybe consistent." Define SLAs for propagation delay (e.g., "read model updated within 2 seconds of write"). Monitor consumer lag. Alert when the bound is breached.
Core concepts
Events are immutable records of state changes. A domain event captures a meaningful business occurrence within a bounded context (OrderPlaced). An integration event crosses context boundaries and should carry only the data consumers need - not the entire aggregate state. Event-carried state transfer includes enough data in the event so consumers never need to call back to the producer.
Event sourcing stores the current state of an entity as a sequence of events rather than a single mutable row. To get current state, replay all events for that aggregate from the event store. Snapshots periodically checkpoint state to avoid replaying the full history. The event store is append-only - never update or delete events. This gives a complete audit trail and enables temporal queries ("what was the order state at 3pm yesterday?").
CQRS (Command Query Responsibility Segregation) splits a service into a command side that handles writes and a query side that handles reads. The command side validates invariants and emits events. The query side subscribes to those events and builds denormalized read models (projections) optimized for specific queries. CQRS does not require event sourcing, and event sourcing does not require CQRS - but they pair naturally because the event log is the bridge between the two sides.
Message brokers are the transport layer. They sit between producers and consumers and handle routing, delivery guarantees, and backpressure. Key broker categories: log-based (Kafka, Redpanda) retain ordered, replayable event logs; queue-based (RabbitMQ, SQS) deliver messages to consumers and remove them after acknowledgment. Choose log-based when you need replay, ordering, and multiple consumer groups. Choose queue-based for simple task distribution and routing flexibility.
Eventual consistency means that after a write, all read replicas and projections will converge to the same state - but not instantly. The gap between write and convergence is the propagation delay. Sagas coordinate multi-service transactions: each step emits an event, and failure triggers compensating events that undo prior steps (e.g., PaymentFailed triggers OrderCancelled). Prefer choreography (services react to events) over orchestration (a central coordinator sends commands) for loosely coupled systems.
Common tasks
Implement event sourcing for an aggregate
Store all state changes as events. Rebuild current state by replaying them.
Event store schema (PostgreSQL example):
CREATE TABLE events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id UUID NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_data JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
version INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (aggregate_id, version)
);Aggregate reconstruction:
def load_aggregate(aggregate_id: str) -> Order:
events = event_store.get_events(aggregate_id)
order = Order()
for event in events:
order.apply(event)
return orderUse the UNIQUE constraint on (aggregate_id, version) for optimistic concurrency. If two commands try to append at the same version, one fails - retry it.
Set up CQRS with separate read/write models
The command side validates and persists events. The query side projects events into denormalized views.
Command side: Receives commands, loads aggregate from event store, validates business rules, appends new events.
Query side: Subscribes to event stream, updates read-optimized projections (e.g., a materialized view in PostgreSQL, an Elasticsearch index, or a Redis hash).
Projection example:
class OrderSummaryProjection:
def handle(self, event):
if event.type == "OrderPlaced":
db.upsert("order_summaries", {
"order_id": event.data["order_id"],
"customer": event.data["customer_name"],
"total": event.data["total"],
"status": "placed"
})
elif event.type == "OrderShipped":
db.update("order_summaries",
where={"order_id": event.data["order_id"]},
set={"status": "shipped"})Keep projections rebuildable. If a projection is corrupted, delete it and replay all events from the store to reconstruct it from scratch.
Choose a message broker
| Requirement | Recommended broker |
|---|---|
| Ordered event log with replay | Kafka or Redpanda |
| Simple task queue with routing | RabbitMQ |
| Serverless / managed queue | AWS SQS + SNS |
| Low-latency pub/sub | NATS |
| Multi-protocol flexibility | RabbitMQ (AMQP, MQTT, STOMP) |
Kafka specifics: Topics are partitioned. Order is guaranteed only within a partition. Use the aggregate ID as the partition key to ensure all events for one entity land on the same partition in order. Consumer groups enable parallel consumption - each partition is read by exactly one consumer in a group.
RabbitMQ specifics: Supports direct, fanout, topic, and header exchanges. Use dead-letter exchanges for failed messages. Prefetch count controls how many unacked messages a consumer holds - set it to prevent memory exhaustion.
Design a saga for distributed transactions
A saga is a sequence of local transactions coordinated through events. Each step has a compensating action that undoes it on failure.
Choreography-based saga (preferred for loose coupling):
OrderService --OrderPlaced--> PaymentService
PaymentService --PaymentSucceeded--> InventoryService
InventoryService --InventoryReserved--> ShippingService
On failure:
PaymentService --PaymentFailed--> OrderService (compensate: cancel order)
InventoryService --InsufficientStock--> PaymentService (compensate: refund)Orchestration-based saga (use when coordination logic is complex): A central OrderSaga orchestrator sends commands to each service and tracks state. Easier to reason about, but the orchestrator is a single point of coupling.
Always define the compensating action for every step before implementing the happy path. If you cannot compensate a step, it must be the last step in the saga.
Handle idempotency in consumers
Duplicate messages are inevitable. Every consumer must handle them safely.
Strategy 1 - Deduplication table:
CREATE TABLE processed_events (
event_id UUID PRIMARY KEY,
processed_at TIMESTAMPTZ DEFAULT now()
);Before processing, check if event_id exists. Use a transaction to atomically insert into processed_events and execute the business logic.
Strategy 2 - Natural idempotency:
Use operations that produce the same result regardless of how many times they run.
SET status = 'shipped' is idempotent. INCREMENT counter is not. Prefer SET-style
operations where possible.
Design event schema and versioning
Schema structure:
{
"event_id": "uuid",
"event_type": "OrderPlaced",
"aggregate_id": "uuid",
"version": 1,
"timestamp": "2026-03-14T10:00:00Z",
"data": {
"order_id": "uuid",
"customer_id": "uuid",
"items": [],
"total": 4999
},
"metadata": {
"correlation_id": "uuid",
"causation_id": "uuid",
"user_id": "uuid"
}
}Versioning strategies:
- Upcasting: Transform old events to the new schema at read time. The event store keeps the original; the reader converts on the fly.
- Schema registry: Use Confluent Schema Registry (Avro/Protobuf) or a custom registry for JSON Schema. Enforce backward compatibility on every schema change.
- Weak schema: Add new fields as optional with defaults. Never remove or rename fields in a non-breaking way.
Always include correlation_id and causation_id in metadata. Correlation ID traces the full business flow; causation ID links to the specific event that caused this one.
Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Using events as remote procedure calls | Tight coupling disguised as events; consumers depend on producer behavior | Events describe what happened, not what should happen next |
| Giant events with full aggregate state | Consumers couple to the producer's internal model; any schema change breaks everyone | Include only the data consumers need; use event-carried state transfer selectively |
| No dead-letter queue | Poison messages block the entire consumer; one bad event stops all processing | Configure a DLQ on every queue; alert on DLQ depth; review and reprocess manually |
| Ordering across partitions | Kafka only guarantees order within a partition; assuming global order causes race conditions | Partition by aggregate ID; accept that cross-aggregate ordering requires explicit coordination |
| Skipping idempotency because "the broker handles it" | At-least-once is the realistic guarantee; exactly-once has caveats and performance costs | Build idempotency into every consumer with dedup tables or natural idempotency |
| Unbounded event store without snapshots | Aggregate reconstruction slows to a crawl as event count grows | Snapshot every N events (e.g., every 100); load from latest snapshot then replay remaining events |
Gotchas
Kafka ordering is per-partition, not per-topic - If two events for the same aggregate land on different partitions (e.g., because the partition key was not the aggregate ID), consumers can process them out of order. Always partition by aggregate ID to guarantee ordering for a single entity.
Projections that fail partway through leave read models in a partially-updated state - If a projection crashes after updating one table but before updating a second, the read model is inconsistent. Either wrap projection updates in a transaction, or design projections to be idempotent and rebuildable from scratch on failure.
No dead-letter queue means one poison message blocks all consumers indefinitely - A malformed event that causes a consumer to throw on every retry will halt the entire consumer group for that partition. Configure a DLQ from day one and alert on DLQ depth. Never leave a consumer group blocked waiting for a poison message to magically self-heal.
Schema changes to events must be backward-compatible or all existing consumers break - Renaming a field, changing a field type, or removing a required field in an event schema breaks every consumer that uses it, including projections built from historical events replayed from the store. Add new fields as optional with defaults; never remove or rename fields without a versioning strategy.
The event store growing without snapshots causes unbounded aggregate reconstruction time - An aggregate with 10,000 events takes 10,000 event loads and applies to reconstruct current state. Plan snapshots before going to production: capture state every N events (e.g., every 100) and store alongside the event log.
References
For detailed content on specific sub-topics, read the relevant file from the
references/ folder:
references/event-sourcing-patterns.md- Advanced event sourcing patterns including snapshots, projections, temporal queries, and event store implementation detailsreferences/broker-comparison.md- Deep comparison of Kafka, RabbitMQ, NATS, SQS/SNS, and Pulsar with configuration examples and operational guidance
Only load a references file if the current task requires it - they are long and will consume context.
References
broker-comparison.md
Message Broker Comparison
Quick Decision Matrix
| Factor | Kafka / Redpanda | RabbitMQ | AWS SQS + SNS | NATS |
|---|---|---|---|---|
| Message retention | Days/weeks (configurable) | Until consumed | 14 days max | No retention (JetStream adds it) |
| Ordering guarantee | Per-partition | Per-queue (single consumer) | FIFO queues only | Per-subject (JetStream) |
| Replay capability | Yes (offset-based) | No | No | JetStream only |
| Throughput | Millions/sec | Tens of thousands/sec | Scales automatically | Millions/sec |
| Routing flexibility | Topics + partitions | Exchanges + routing keys | SNS filters | Subjects with wildcards |
| Ops complexity | High (ZooKeeper/KRaft, partitions) | Medium | Zero (managed) | Low |
| Best for | Event streaming, log aggregation | Task queues, RPC, routing | Serverless, AWS-native | Microservice messaging, IoT |
Apache Kafka / Redpanda
Core model
Kafka organizes messages into topics, each split into partitions. Producers write to a topic; the partition is chosen by the message key (hash) or round-robin. Consumers read from partitions in consumer groups - each partition is assigned to exactly one consumer in a group.
Key configuration
| Config | Default | Recommendation |
|---|---|---|
replication.factor |
1 | Set to 3 for production |
min.insync.replicas |
1 | Set to 2 (with replication.factor=3) |
acks (producer) |
1 | Set to all for durability |
enable.auto.commit |
true | Set to false; commit offsets manually after processing |
max.poll.records |
500 | Tune based on consumer processing time |
retention.ms |
7 days | Set based on replay requirements |
Partition key strategy
- Use the aggregate ID or entity ID as the key to guarantee ordering for that entity
- All events for the same key land on the same partition
- Never use a random or null key if ordering matters
- Avoid hot partitions - if one key produces vastly more events, use a compound key
Consumer group patterns
Competing consumers: Multiple consumers in one group share the load. Each partition is read by one consumer. Scale consumers up to the number of partitions.
Fan-out: Multiple consumer groups each read all messages independently. Use for different projections, analytics, and audit systems consuming the same events.
When to use Kafka
- Event sourcing with replay requirements
- High-throughput event streaming (>100k events/sec)
- Multiple consumers need independent reads of the same stream
- You need a durable, ordered event log
- Log aggregation and change data capture (CDC)
Redpanda vs Kafka
Redpanda is wire-compatible with Kafka but eliminates JVM and ZooKeeper dependencies. Written in C++, it provides lower latency and simpler operations. Use Redpanda when you want Kafka semantics without the operational overhead.
RabbitMQ
Core model
RabbitMQ uses exchanges, queues, and bindings. Producers send messages to an exchange; the exchange routes to queues based on bindings and routing keys. Consumers read from queues.
Exchange types
| Type | Routing behavior |
|---|---|
| Direct | Routes to queues where binding key exactly matches routing key |
| Fanout | Routes to all bound queues (broadcast) |
| Topic | Routes based on wildcard pattern matching on routing key |
| Headers | Routes based on message header attributes |
Key configuration
| Config | Recommendation |
|---|---|
| Prefetch count | Set to 10-50 per consumer; prevents one consumer from hoarding messages |
| Message TTL | Set per-queue to prevent unbounded growth |
| Dead letter exchange | Configure on every queue; catch poison messages |
| Publisher confirms | Enable for durability; fire-and-forget loses messages |
| Quorum queues | Use instead of classic mirrored queues for replication |
Reliability pattern
Producer -> Exchange -> Queue -> Consumer
| | |
+-- publisher confirm | +-- manual ack
|
+-- dead letter exchange -> DLQ- Producer sends with publisher confirms enabled
- Broker acknowledges receipt
- Consumer processes and sends manual ACK
- If consumer fails or rejects, message goes to dead letter exchange
- Monitor DLQ depth; alert on non-zero
When to use RabbitMQ
- Task queues (background jobs, email sending)
- Complex routing requirements (topic exchanges, header-based routing)
- RPC-style request/reply patterns
- When you need message-level TTL and priority queues
- Moderate throughput (<50k messages/sec)
AWS SQS + SNS
Core model
SNS (Simple Notification Service) is a pub/sub topic. SQS (Simple Queue Service) is a message queue. Combine them: SNS fan-out to multiple SQS queues.
SQS variants
| Feature | Standard Queue | FIFO Queue |
|---|---|---|
| Ordering | Best-effort | Strict per message group |
| Delivery | At-least-once | Exactly-once (with dedup) |
| Throughput | Unlimited | 3,000 msg/sec (with batching) |
| Deduplication | None | Content-based or explicit dedup ID |
Key configuration
| Config | Recommendation |
|---|---|
| Visibility timeout | Set to 6x your average processing time |
| Receive wait time | 20 seconds (enable long polling; reduce empty receives) |
| Redrive policy | Max receives = 3-5 before sending to DLQ |
| Message retention | 4-14 days based on recovery needs |
When to use SQS + SNS
- AWS-native architecture
- Serverless (Lambda consumers)
- You want zero operational overhead
- Fan-out from SNS to multiple SQS queues
- FIFO ordering per message group is sufficient
NATS
Core model
NATS is a lightweight, high-performance messaging system. Core NATS is pure pub/sub with no persistence. JetStream adds persistence, replay, and consumer groups.
Subject-based addressing
orders.placed - specific event
orders.* - wildcard: any event in orders
orders.> - multi-level wildcard: orders.placed, orders.us.placedWhen to use NATS
- Microservice-to-microservice communication
- Low-latency requirements (<1ms)
- IoT and edge computing
- Simple pub/sub without complex routing
- When Kafka is overkill for your throughput needs
Broker Anti-Patterns
| Anti-pattern | Problem | Solution |
|---|---|---|
| Using Kafka as a database | Compacted topics lose event history; query support is poor | Use Kafka as transport; store in a proper database |
| Auto-committing offsets in Kafka | Messages marked as consumed before processing completes; data loss on crash | Manual commit after successful processing |
| RabbitMQ with unlimited prefetch | One fast consumer starves others; memory exhaustion on consumer failure | Set prefetch to 10-50 |
| No dead-letter queue | Poison messages block the entire queue forever | Always configure DLQ; alert on depth |
| Synchronous publish in request path | Broker latency adds to user-facing response time; broker outage blocks requests | Publish asynchronously; use an outbox pattern if durability is needed |
| Choosing a broker before defining requirements | Ends up with Kafka for a task queue or RabbitMQ for event replay | Start from requirements: ordering, replay, throughput, ops budget |
The Transactional Outbox Pattern
When you need to atomically update a database and publish an event (dual write problem):
- Write the event to an "outbox" table in the same database transaction as the business data
- A separate process (poller or CDC) reads the outbox table and publishes to the broker
- After successful publish, mark the outbox entry as published
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
published BOOLEAN DEFAULT false
);This guarantees that the database write and the event publish either both happen or neither happens - solving the dual write problem without distributed transactions.
event-sourcing-patterns.md
Event Sourcing Patterns
Event Store Implementation
The event store is the single source of truth in an event-sourced system. It is an append-only log of domain events, keyed by aggregate ID.
Core operations
| Operation | Description |
|---|---|
| Append | Add new events for an aggregate at a specific expected version |
| Load | Retrieve all events for an aggregate (optionally from a snapshot) |
| Subscribe | Stream new events as they are appended (for projections) |
Optimistic concurrency
Every append includes an expected version number. If the current version in the store does not match, the append fails with a concurrency conflict. The caller must reload the aggregate, re-validate business rules, and retry.
def append_events(aggregate_id, events, expected_version):
current_version = store.get_latest_version(aggregate_id)
if current_version != expected_version:
raise ConcurrencyConflict(
f"Expected version {expected_version}, got {current_version}"
)
for i, event in enumerate(events):
event.version = expected_version + i + 1
store.insert(event)Storage backends
| Backend | Pros | Cons |
|---|---|---|
| PostgreSQL + events table | Familiar, ACID, easy to query | Requires manual subscription mechanism (LISTEN/NOTIFY or polling) |
| EventStoreDB | Purpose-built, built-in subscriptions, projections | Operational overhead of a specialized database |
| DynamoDB + streams | Serverless, auto-scaling, built-in change streams | Partition key design is critical; 1MB item limit |
| Kafka (as event store) | High throughput, built-in replication | Log compaction complicates aggregate reconstruction; not a natural fit |
Recommendation: Start with PostgreSQL for simplicity. Move to EventStoreDB when you need built-in subscriptions and projections at scale.
Snapshots
Snapshots periodically checkpoint the aggregate state to avoid replaying the full event history on every load.
When to snapshot
- Every N events (e.g., every 100 events per aggregate)
- When aggregate reconstruction exceeds a latency threshold (e.g., >50ms)
- Never snapshot on every event - the overhead defeats the purpose
Snapshot schema
CREATE TABLE snapshots (
aggregate_id UUID NOT NULL,
aggregate_type VARCHAR(100) NOT NULL,
version INTEGER NOT NULL,
state JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (aggregate_id)
);Load with snapshot
def load_aggregate(aggregate_id):
snapshot = snapshot_store.get_latest(aggregate_id)
if snapshot:
aggregate = deserialize(snapshot.state)
events = event_store.get_events(
aggregate_id, after_version=snapshot.version
)
else:
aggregate = Order()
events = event_store.get_events(aggregate_id)
for event in events:
aggregate.apply(event)
return aggregateSnapshot invalidation
Snapshots are never invalidated - they are a cache of a point-in-time state. If the aggregate model changes (new fields, different apply logic), delete all snapshots and let them rebuild lazily.
Projections
Projections (also called read models or materializers) subscribe to the event stream and build denormalized views optimized for specific queries.
Types of projections
| Type | Description | Use case |
|---|---|---|
| Inline projection | Built synchronously as part of the command handler | When strong read-after-write consistency is required |
| Async projection | Built by a background consumer subscribing to events | Default choice; decouples read from write |
| Catch-up projection | Replays historical events to build a new read model | When adding a new query requirement to an existing system |
Projection lifecycle
- Start from position zero - subscribe to the event stream from the beginning
- Process each event - update the read model based on event type
- Track position - store the last processed event position/offset
- Handle restarts - resume from the last stored position
class ProjectionRunner:
def __init__(self, projection, position_store):
self.projection = projection
self.position_store = position_store
def run(self):
last_position = self.position_store.get(self.projection.name)
for event in event_store.subscribe(after=last_position):
self.projection.handle(event)
self.position_store.save(self.projection.name, event.position)Rebuilding projections
If a projection is corrupted or the schema changes:
- Delete the projection data
- Reset the position to zero
- Replay all events
This is one of the major benefits of event sourcing - projections are disposable and rebuildable.
Temporal Queries
Event sourcing enables temporal queries that are impossible with mutable state.
Point-in-time reconstruction
def get_state_at(aggregate_id, timestamp):
events = event_store.get_events(
aggregate_id, up_to=timestamp
)
aggregate = Order()
for event in events:
aggregate.apply(event)
return aggregateEvent history for audit
def get_audit_trail(aggregate_id):
events = event_store.get_events(aggregate_id)
return [
{
"when": e.created_at,
"what": e.event_type,
"who": e.metadata.get("user_id"),
"data": e.event_data
}
for e in events
]Retroactive corrections
When a business rule was wrong and past events need correction:
- Never delete or update existing events
- Append a compensating event (e.g., OrderAmountCorrected)
- The compensating event adjusts the aggregate state going forward
- Projections replay and self-correct
Event Upcasting
When event schemas evolve, upcasters transform old event formats to new ones at read time.
class OrderPlacedV1ToV2Upcaster:
def can_upcast(self, event):
return event.type == "OrderPlaced" and event.schema_version == 1
def upcast(self, event):
event.data["currency"] = "USD" # new required field with default
event.schema_version = 2
return eventUpcasting rules
- Upcasters run in a chain: V1 -> V2 -> V3
- Never modify the stored event - upcast only at read time
- Keep upcasters simple - field additions with defaults, field renames
- If the transformation is complex, consider a one-time migration that appends new-format events and marks old ones as superseded
Aggregate Design Guidelines
| Guideline | Rationale |
|---|---|
| Keep aggregates small | Fewer events to replay; fewer concurrency conflicts |
| One aggregate per transaction | Cross-aggregate consistency requires sagas |
| Aggregate boundaries = consistency boundaries | Everything inside an aggregate is strongly consistent |
| Reference other aggregates by ID only | Prevents coupling and enables independent scaling |
| Apply events, not commands, to state | The apply method must be pure - no side effects, no validation |
Frequently Asked Questions
What is event-driven-architecture?
Use this skill when designing event-driven systems, implementing event sourcing, applying CQRS patterns, selecting message brokers, or reasoning about eventual consistency. Triggers on tasks involving Kafka, RabbitMQ, event stores, command-query separation, domain events, sagas, compensating transactions, idempotency, message ordering, and any architecture where components communicate through asynchronous events rather than direct synchronous calls.
How do I install event-driven-architecture?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill event-driven-architecture in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support event-driven-architecture?
event-driven-architecture works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.