Home Resume
Homeβ€Ί Blogβ€Ί AWS Architecture Series #4 β€” EventBridge: …
AWS Architecture AWS Architecture Series

EventBridge: The Enterprise Event Bus Enterprises Get Wrong

Tight coupling is not a code problem. It is a deployment problem, an operational problem, and a team coordination problem. EventBridge solves it structurally β€” but only when you replace the synchronous calls, not supplement them.

Business Challenge

An e-commerce platform processes orders through a monolithic Order Service. When a customer places an order, the service makes four synchronous HTTP calls: reserve inventory, send a confirmation email, write an analytics event, and trigger fulfilment. Four calls, four failure modes, four teams whose uptime directly affects checkout conversion.

The Notifications team deploys a buggy release on Black Friday. Checkout fails for 40 minutes. The Analytics team's Kinesis stream backs up during a flash sale; every checkout request slows by 800ms waiting for a write that a customer never sees. Adding a fifth consumer β€” a fraud scoring service β€” requires a code change in Order Service, a new HTTP client, a new retry policy, and a coordinated deploy with the fraud team.

The team decides to adopt Amazon EventBridge. They add events.PutEvents() at the end of the order handler. Now the order saves to the database, calls Inventory synchronously, calls Notifications synchronously, calls Analytics synchronously β€” and then also fires an event. They have added a sixth communication channel without removing any of the original five. The blast radius is unchanged.

EventBridge does not reduce coupling by being present

EventBridge reduces coupling when services that previously received direct calls now only receive events β€” and the producer has no knowledge of who those consumers are. Adding PutEvents alongside existing synchronous calls is instrumentation, not architecture. The decoupling happens when you replace the calls, not supplement them.

Architecture

EventBridge routes events from producers to consumers through rules on an event bus. The producer calls PutEvents with a structured JSON payload; EventBridge evaluates every rule on the target bus in parallel and delivers matching events to each rule's targets. The producer is finished at the point of PutEvents β€” it does not wait for consumers, does not know which rules matched, and does not know which targets received the event.

Diagram showing the before state (Order Service making four synchronous HTTP calls to Inventory, Notifications, Analytics, and Fulfilment) versus the after state (Order Service publishing one event to EventBridge which routes to four independent targets via rules)
Before/after: synchronous fan-out versus EventBridge fan-out. The topology looks similar, but the dependency direction reverses. In the before state, Order Service depends on all four consumers. In the after state, the four consumers depend on EventBridge β€” Order Service depends on nothing downstream.

Event bus and event structure

EventBridge has a default event bus that receives events from AWS services (EC2 state changes, S3 object operations, CloudTrail API calls, and more than 200 other sources). For application events, create a custom event bus. Custom buses are isolated from the default bus, support resource-based policies for cross-account access, and have cleaner rule namespacing. One bus per domain boundary β€” Orders, Auth, Inventory β€” is a reasonable starting point.

Every event has a fixed envelope: source (who produced it), detail-type (what happened), and detail (the payload, up to 256 KB as JSON). Rules match on source, detail-type, and fields inside detail. The schema of the detail object is yours to define β€” EventBridge does not validate it unless you enable schema discovery.

Diagram showing the full EventBridge component architecture: producers (Order Service, Auth Service, AWS Services) sending events to a custom event bus, which routes through rules to targets (SQS, Lambda, Kinesis, Step Functions), with Schema Registry above the bus and Event Archive below, and DLQ on each target
Full component architecture. The custom event bus is the centre. Schema Registry validates and documents what flows through it. Event Archive persists everything for replay. Rules route to targets β€” each target has its own DLQ so a consumer failure does not affect other consumers.

Rules and targets

A rule is a filter plus a target list. The filter is an event pattern β€” a JSON object that specifies values to match on fields in the event envelope. EventBridge evaluates all rules on the bus whenever an event arrives; there is no concept of routing priority. An event that matches three rules is delivered to all three independently.

Each rule can route to up to five targets. Targets include Lambda functions, SQS queues, SNS topics, Kinesis Data Streams, Step Functions state machines, API destinations (arbitrary HTTPS endpoints), and other EventBridge buses. EventBridge delivers events to each target with at-least-once semantics β€” if delivery fails, EventBridge retries for up to 24 hours before routing to the rule's dead-letter queue (if configured).

Input transformation: decouple event shape from consumer shape

Each target can have an input transformer that reshapes the event before delivery. A Lambda function expecting a specific payload schema does not need to know the EventBridge envelope format β€” the transformer extracts the fields it needs. This lets you evolve the event schema without changing consumer code, and lets consumers with different expected formats subscribe to the same event.

Schema Registry

EventBridge can auto-discover schemas from events flowing through a bus and store them as OpenAPI 3.0 documents. Once discovered, you generate typed code bindings (Java, Python, TypeScript, Go) directly from the console or CLI β€” no manual schema maintenance. Teams consuming an event import the generated types rather than parsing untyped JSON. Schema versioning is tracked automatically.

Event Archive and Replay

An archive captures all events matching a filter pattern, with configurable retention from 1 day to indefinite. Replay reprocesses archived events through the current rules β€” useful for backfilling a new consumer, recovering from a consumer bug without needing the producer to resend, or reproducing a production incident in a test environment against real event data.

Dead-Letter Queues

Configure a DLQ (SQS queue) on each target, not on the bus. When EventBridge exhausts retries for a specific target, it routes the failed event to that target's DLQ with metadata about the failure. Each consumer has its own failure queue β€” a Lambda DLQ filling up does not affect SQS or Kinesis targets on the same rule.

Cross-Account Routing

EventBridge buses accept events from other accounts through resource-based policies. The sending account puts events to a bus ARN in the receiving account; the receiving account applies its own rules. This is the standard pattern for platform teams emitting events (security findings, cost alerts, config drift) that multiple product accounts need to consume without polling a shared API.

Why This Architecture Works

Tight coupling is a deployment problem, not a code problem

When Order Service calls Notifications directly, both teams must coordinate releases. A breaking change in the Notifications API requires Order Service to update before Notifications can deploy. EventBridge inverts this: Notifications subscribes to order.placed events and parses what it needs. Notifications can change its internal data model freely. Order Service can add fields to the event without coordinating with consumers. The constraint that forced joint deploys disappears.

Consumer isolation is structural, not operational

In a synchronous fan-out, a slow consumer slows the producer. An unavailable consumer blocks the producer. EventBridge removes both constraints structurally: PutEvents completes when the event is accepted by EventBridge, not when consumers finish processing. A Lambda target timing out on a slow downstream API does not affect the SQS target or the Kinesis target on the same rule. The retry window (up to 24 hours) and the DLQ give the consumer time to recover without the producer ever knowing there was a problem.

Adding consumers is a consumer-side operation

A fraud scoring team wants to evaluate every order. In the synchronous model, Order Service adds an HTTP call and a new deploy dependency. With EventBridge, the fraud team creates a new rule on the orders bus matching order.placed, points it at their Lambda, and deploys independently. Order Service code does not change. Order Service engineers do not review the change. The blast radius of the fraud team's Lambda crashing is their DLQ, not checkout.

Key Design Decisions

1
One event bus per domain boundary, not one per service

A common mistake is creating one bus per microservice β€” an OrderService bus, a PaymentService bus, an InventoryService bus. This recreates the coupling in bus form: consumers must know which service bus to subscribe to, and moving logic between services requires migrating bus subscriptions. Bus per domain β€” Orders, Fulfilment, Platform β€” keeps the event namespace stable even as services are split, merged, or renamed.

Rule

Name the bus after the domain, not the producer. If two services both emit events that consumers treat as part of the same business domain, they belong on the same bus. If an event could plausibly come from either service and consumers should not care which, the domain boundary is the right granularity.

2
Event-carried state transfer vs event notification

An event notification tells consumers something happened: {"source": "orders", "detail-type": "order.placed", "detail": {"orderId": "abc123"}}. A consumer that needs the order contents must call the Orders API to fetch them β€” now there is a synchronous dependency through the back door. Event-carried state transfer includes the data consumers need: {"orderId": "abc123", "customerId": "...", "items": [...], "total": 142.50}. Consumers are truly independent.

Decision signal

If more than one consumer would need to call the producer's API after receiving the event, add that data to the event payload. The 256 KB limit is generous for most transactional events. For large payloads (document content, images, large arrays), put the object in S3 and include the S3 key in the event β€” consumers fetch the object directly rather than through the producer.

3
Ordering guarantees and idempotency

EventBridge provides at-least-once delivery β€” duplicates are possible, especially during retries. There is no guaranteed ordering between events: two events published milliseconds apart may arrive at a Lambda target in either order. If your consumer logic is not idempotent (processing an event twice produces a different result than processing it once), EventBridge will cause subtle bugs that are hard to reproduce because they only appear under retry conditions.

Required pattern

Consumers must be idempotent. The standard approach is to include a unique event ID in the payload and use a DynamoDB conditional write β€” if the ID already exists in a processed-events table, skip the business logic and return success. For ordered processing (inventory reservation must precede fulfilment), use a Step Functions workflow triggered by the event rather than multiple Lambda consumers on the same event.

4
Schema governance across teams

EventBridge schema discovery is opt-in per bus. Without it, event schemas live in team wikis, Slack messages, and the institutional memory of the engineer who wrote the producer. Two consumers that both parse the same event diverge silently when the producer changes field names. Schema Registry with auto-discovery gives you a central, versioned source of truth β€” but discovery only captures schemas of events that actually flowed through the bus, which means a low-volume event type may not have a registered schema when a new consumer team needs it.

Governance pattern

Enable schema discovery on all custom buses. Supplement it with explicit schema registration for event types that are part of your platform contract β€” events that cross team or account boundaries. Treat the EventBridge Schema Registry as the API contract between teams: if it is not in the registry, it is not a supported event type.

Tradeoffs and Alternatives

Approach Works well when Breaks down when
EventBridge custom bus Fan-out to 2–5 consumers; mixed target types (Lambda, SQS, Step Functions); cross-account event routing; low-to-medium throughput (up to ~10,000 events/sec per bus) Throughput exceeds 10,000 events/sec sustained (use Kinesis); strict event ordering required; consumer needs to process a stream of related events with stateful aggregation
SNS + SQS fan-out Simple pub/sub with SQS consumer buffering; teams already using SNS; need message filtering with SNS filter policies Cross-account routing is more complex than EventBridge; no schema registry; no archive/replay; filter logic is less expressive than EventBridge event patterns
Kinesis Data Streams High-throughput ordered event streams (100,000+ events/sec); consumers need to process events in sequence; analytics or ML pipelines requiring replay over a time window Fan-out to heterogeneous consumer types is more complex; provisioned shard capacity requires planning; EventBridge Pipes can bridge EventBridge β†’ Kinesis for hybrid architectures
SQS point-to-point Single consumer that must process every message exactly once (with deduplication); work queue semantics where the producer does not care about consumer identity Multiple consumers need the same message (use SNS or EventBridge to fan out to multiple SQS queues); event history or schema documentation required

Reference: EventBridge Rule with SQS Target and DLQ

A Terraform configuration that creates an EventBridge rule matching order.placed events, routes them to an SQS queue for the Inventory service, and configures a DLQ for failed deliveries:

Terraform β€” EventBridge Rule β†’ SQS with DLQ
resource "aws_cloudwatch_event_bus" "orders" {
  name = "orders"
}

resource "aws_cloudwatch_event_rule" "order_placed_inventory" {
  name           = "order-placed-inventory"
  event_bus_name = aws_cloudwatch_event_bus.orders.name
  description    = "Route order.placed events to Inventory SQS queue"

  event_pattern = jsonencode({
    source      = ["com.mycompany.orders"]
    detail-type = ["order.placed"]
  })
}

resource "aws_sqs_queue" "inventory_dlq" {
  name = "inventory-order-events-dlq"
}

resource "aws_sqs_queue" "inventory" {
  name                      = "inventory-order-events"
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.inventory_dlq.arn
    maxReceiveCount     = 3
  })
}

resource "aws_cloudwatch_event_target" "inventory_sqs" {
  rule           = aws_cloudwatch_event_rule.order_placed_inventory.name
  event_bus_name = aws_cloudwatch_event_bus.orders.name
  target_id      = "InventorySQS"
  arn            = aws_sqs_queue.inventory.arn

  dead_letter_config {
    arn = aws_sqs_queue.inventory_dlq.arn
  }
}

The rule's DLQ and the SQS queue's own redrive policy serve different failure modes. The rule DLQ captures events that EventBridge could not deliver to the SQS queue (queue unavailable, permissions error). The SQS redrive policy captures messages the Inventory consumer failed to process after receiving them. Both are necessary; neither is redundant.

Closing Thought

Tight coupling is not a code problem. It is a deployment problem, an operational problem, and a team coordination problem. EventBridge solves it structurally β€” not by making failures invisible, but by making them bounded. A consumer failing affects only that consumer's DLQ, not the producer and not other consumers. A consumer team deploying does not require a joint release with the producer team.

The common mistake is treating EventBridge as a notification layer added on top of an existing synchronous architecture. That pattern doubles the communication channels without reducing the blast radius of any one of them. The useful pattern is substitution: replace the synchronous call with an event, let the consumer subscribe, and resist the urge to add a synchronous fallback "just in case." The fallback recreates the coupling you are trying to remove.

Start with one domain boundary. Pick the downstream call that causes the most deployment friction β€” the one that forces joint releases, the one whose outages affect upstream conversion, the one where adding a new consumer requires a producer code change. Replace that call with an EventBridge event. Validate the pattern at one boundary before expanding. The architecture compounds; you do not need to do it all at once.

Next in this series

Security β€” KMS key hierarchy for multi-account encryption: how to design Customer Managed Keys, key policies, and grants so that encryption is a platform service rather than a per-team implementation detail that drifts across accounts.

Official AWS Reference

AWS Documentation β€” Amazon EventBridge User Guide β€” covers event buses, rules, targets, event patterns, schema registry, archive and replay, and cross-account event routing. The event pattern syntax reference is particularly useful when building precise rule filters.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent