Event-Driven Architecture skill for Codex

Events that survive duplicates, reordering and failure. Codex designs events as owned, versioned contracts with a CloudEvents-style envelope and a registry compatibility check, chooses Kafka, RabbitMQ, SQS, EventBridge or Pub/Sub from actual needs, and keys partitions by the entity that needs ordering. Consumers dedupe on event IDs inside the same transaction as their side effects, and every subscription gets bounded retries, a watched dead-letter queue and a replay procedure.

  • Architecture
  • $event-driven-architecture
  • 192 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- event-driven-architecture

Installs to ~/.agents/skills/event-driven-architecture/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $event-driven-architecture, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs event-driven systems that survive production - event types and naming, CloudEvents envelopes, schema evolution with a registry, broker choice (Kafka, RabbitMQ, SQS/SNS, EventBridge, Pub/Sub, NATS), partition keys for ordering, at-least-once delivery with idempotent consumers, retries, dead-letter queues and tracing across async hops. Use when introducing events or a message broker, defining an event contract or AsyncAPI spec, debugging duplicate or out-of-order messages, or reviewing a publish/subscribe design.

Use it when

  • Introducing a message broker or the first published events between services
  • Defining or evolving an event schema or AsyncAPI contract
  • Consumers see duplicates, reordering or silently lost messages
  • Reviewing a publish/subscribe design before it ships

Use something else when

How to check the result

Integration tests deliver each event twice and out of order with the same final state, a killed consumer loses and duplicates nothing on restart, and a poison message lands in the DLQ and replays after a fix.

The mistake it prevents

Trusting broker 'exactly-once' settings and skipping consumer idempotency, so a redelivery after a crash charges, emails or ships twice.

~/.agents/skills/event-driven-architecture/SKILL.md

--- name: event-driven-architecture description: Designs event-driven systems that survive production - event types and naming, CloudEvents envelopes, schema evolution with a registry, broker choice… license: MIT ---

An event-driven design is working when every event has one owner and a versioned schema, consumers produce the same result whether a message arrives once or five times, ordering is guaranteed exactly where the business needs it and nowhere else, and a failed message ends up somewhere a human can see and replay.

Decide whether events fit

NeedEvents?
Several independent reactions to one fact (email, analytics, search index)Yes, publish/subscribe
Absorb bursts, decouple producer availability from consumerYes, queue
Caller needs an answer now to continue (price, auth decision)No, synchronous call
Multi-step business transaction across servicesEvents plus a saga ($saga-pattern)
Two modules in one deployableIn-process events or direct calls first ($modular-monolith)

Kinds of messages

KindExamplePayloadCoupling
Event notificationOrderPlaced {orderId}IDs only; consumers call back for detailsLow payload, runtime coupling via callbacks
Event-carried state transferOrderPlaced {orderId, lines, total, customer}Everything consumers needNo callbacks; schema coupling
Command (message)SendInvoice {orderId}Intent to one handlerDirected; not pub/sub
Domain event (internal)Raised inside an aggregateDomain typesPrivate to the service

Default for integration events: event-carried state transfer with the fields current consumers need, so a slow or down producer does not break consumers. Name events in past tense business language (PaymentCaptured), never CRUD (PaymentUpdated) and never commands disguised as events.

Envelope and schema

Use the CloudEvents attributes even if you do not use a CloudEvents SDK:

{
  "specversion": "1.0",
  "id": "5f0c2a8e-3b1f-4c7e-9a51-2f8d1e6c0b44",
  "source": "/services/orders",
  "type": "com.acme.orders.order_placed.v1",
  "subject": "order/ord_8412",
  "time": "2026-03-14T10:22:31Z",
  "datacontenttype": "application/json",
  "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "data": { "orderId": "ord_8412", "customerId": "cus_19", "total": { "amount": "84.10", "currency": "EUR" } }
}
  • id is unique per event and stable across redeliveries; consumers dedupe on it.
  • traceparent (W3C Trace Context, also the CloudEvents distributed tracing extension) links async hops in traces.
  • Money as string decimals plus currency; timestamps in UTC ISO 8601.

Schema evolution rules (Avro, Protobuf or JSON Schema, checked by a schema registry or a CI compatibility test):

ChangeSafe?
Add optional field with defaultYes (backward compatible)
Remove optional field consumers do not useYes after checking consumers
Rename field, change type, make optional requiredNo: publish ...v2 alongside v1, migrate consumers, retire v1
Change meaning of a field without changing its nameNever

Set registry compatibility to BACKWARD (or BACKWARD_TRANSITIVE for long retention topics). Document contracts in an AsyncAPI 3 file next to the producer.

Broker choice

BrokerStrengthsPick when
Kafka (or Redpanda, MSK, Confluent)Ordered partitions, retention and replay, high throughputEvent streams many consumers read at their own pace; replay matters
RabbitMQFlexible routing, per-message acks, prioritiesWork queues and routing-heavy command messaging
SQS + SNSFully managed, simple, FIFO option with group IDsAWS shop, moderate throughput, no replay need
EventBridgeManaged routing by rules, SaaS sourcesAWS integration events between teams/accounts
Google Pub/SubManaged, ordering keys, push or pullGCP shop
NATS JetStreamLight, fast, simple opsEdge, IoT, low-ops streaming

Default: the managed queue of your cloud for work distribution; Kafka only when you need replay, long retention or very high throughput and can operate it.

Ordering

  • Brokers order only within a partition, FIFO group or ordering key. Choose the key as the entity whose events must be ordered (orderId), never a random value.
  • Global ordering does not scale and is rarely needed. Ask which entity needs order.
  • Consumers must still tolerate reordering across keys and redelivery: compare an entity version or sequence in the payload and ignore stale updates.
  • Hot keys (one huge tenant) bottleneck a single partition; consider a compound key if per-tenant ordering is not required.

Delivery and idempotency

Assume at-least-once delivery everywhere. “Exactly-once” broker features (Kafka transactions) cover read-process-write within Kafka only; side effects in databases, emails or APIs still need idempotent handling.

CREATE TABLE processed_messages (
  consumer   text NOT NULL,
  message_id text NOT NULL,
  processed_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (consumer, message_id)
);
def handle(event: dict) -> None:
    with db.transaction() as tx:
        inserted = tx.execute(
            "INSERT INTO processed_messages (consumer, message_id) VALUES (%s, %s) "
            "ON CONFLICT DO NOTHING",
            ("billing", event["id"]),
        ).rowcount
        if inserted == 0:
            return                      # duplicate: already applied
        apply_business_change(tx, event["data"])
    # commit/ack the message only after the transaction commits

Publishing reliably from a service that also writes to its database needs the outbox, not “write then publish” ($outbox-pattern).

Kafka producer settings worth checking: acks=all, enable.idempotence=true (default in modern clients), and a min.insync.replicas of 2 on topics with replication factor 3. Consumers: commit offsets after processing, not before.

Failure handling

attempt -> transient error -> retry with exponential backoff + jitter (bounded, e.g. 5 tries)
        -> permanent error (validation, unknown schema) -> dead-letter immediately
        -> retries exhausted -> dead-letter with error, attempt count, original headers
  • Every queue/subscription has a DLQ with an alert on depth > 0 and a documented replay command. A DLQ nobody watches is data loss with extra steps.
  • Do not block a Kafka partition forever on one poison message; route it to a retry or dead-letter topic and continue.
  • Set visibility timeout (SQS) or ack deadline (Pub/Sub) above the p99 handling time.

Workflow

- [ ] 1. Read AGENTS.md and existing messaging code; list current producers and consumers
- [ ] 2. Name the event, its owner, and consumers; choose state transfer vs notification
- [ ] 3. Write the schema and envelope; register it or add a compatibility check
- [ ] 4. Choose the partition/ordering key from the entity that needs order
- [ ] 5. Publish via outbox; consume idempotently; ack after commit
- [ ] 6. Configure retries, DLQ, alerts and a replay procedure
- [ ] 7. Propagate trace context; add lag and DLQ dashboards
- [ ] 8. Document in AsyncAPI; add a contract test between producer and consumer

Gotchas

  • Event chains that implement a hidden synchronous workflow (A emits, B reacts and emits, C reacts…) are hard to reason about. When the steps form one business process, make it explicit with an orchestrated saga.
  • Consumers that call back to the producer for every event re-create the runtime coupling events were supposed to remove.
  • Using events to replicate an entire database to other services creates a distributed monolith. Publish facts consumers need, not every column change.
  • PII in events lives as long as topic retention and every consumer’s copy. Minimise it and plan erasure (tokenise or encrypt per subject).
  • Changing a partition count on a Kafka topic changes key-to-partition mapping and breaks per-key ordering during the transition.

Verification

  • Deliver every test event twice and out of order in integration tests; state must match single in-order delivery.
  • Kill a consumer mid-batch; on restart no message is lost and no side effect is doubled.
  • Schema compatibility check fails CI when a field is renamed.
  • A poison message reaches the DLQ, fires the alert, and the replay command restores it after a fix.

Output contract

**Event:** com.acme.orders.order_placed.v1 (owner: orders) -> consumers: billing, email, search
**Schema:** schemas/order_placed.v1.avsc, compatibility BACKWARD, AsyncAPI: docs/asyncapi.yaml
**Ordering key:** orderId | **Broker/topic:** orders.events (12 partitions, RF 3)
**Delivery:** outbox -> at-least-once; consumers dedupe on event id
**Failure path:** 5 retries with backoff -> orders.events.dlq, alert on depth > 0, replay: <cmd>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Event-Driven Architecture"
  short_description: "Events that survive duplicates, reordering and failure"
  default_prompt: "$event-driven-architecture Design the events for our order lifecycle so billing, email and search can react independently"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc