Filter concepts by levelShowing all levels.

AWS · Section 28

EventBridge and Event-Driven Architecture

Level
advanced
Read
20 min
Concepts
3

EventBridge routes events from a source, through a bus, to whichever targets a rule's event pattern (or schedule) matches — up to five per rule, invoked in parallel, with retries and an optional dead-letter queue on failure, and an archive that can replay matched events back to their source bus later. Building on top of that vocabulary, the domain-events-vs-commands distinction decides what belongs on the bus at all: a past-tense announcement with no known subscriber count fits an event bus, while a one-handler instruction expecting a direct result does not. The event-driven design payoff — producers and subscribers deploying independently — is inseparable from four problems the platform pushes onto every subscriber: the event's shape can change, delivery is at-least-once so duplicates are normal, failed deliveries retry automatically, and a subscriber's own copy of the world is only ever eventually consistent with the source.

This section

What is true here

  1. A rule matches events by pattern or schedule and fans out to up to 5 targets, invoked in parallel with no ordering between them.
  2. A domain event announces something that already happened to an unknown number of subscribers; a command instructs one specific handler and expects a result.
  3. EventBridge delivery is at-least-once — a handler must dedup on the event's unique id to stay safe against duplicate or retried delivery.
  4. A failed target retries per the rule's RetryPolicy, then lands in a configured dead-letter queue instead of being silently dropped.
  5. Loose coupling trades independent deployability for four problems every subscriber owns: versioning, duplicates, retries, and eventual consistency.

What you will be able to do

  • Design an event pattern that matches only the intended source and event type, not an entire producer's output
  • Decide whether a given interaction belongs on an event bus (a domain event) or as a direct call (a command)
  • Build an event handler that is safe against duplicate delivery by deduplicating on the event id
  • Evolve an event's shape without breaking subscribers that deploy independently of the producer
From the EventBridge vocabulary to loosely coupled design
routesshapes what a subscribermust tolerate

Buses, rules, targets, patterns

Domain events vs commands

Versioning, duplicates, retries, eventual consistency

  • Buses, rules, targets, patterns
    • leads to Domain events vs commands (routes)
  • Domain events vs commands
    • leads to Versioning, duplicates, retries, eventual consistency (shapes what a subscriber must tolerate)
  • Versioning, duplicates, retries, eventual consistency

EventBridge and Event-Driven Architecture

The EventBridge vocabulary (buses, rules, targets, patterns, schedules, archive/replay, cross-account), the domain-events-vs-commands distinction, and the coupling concerns — versioning, duplicates, retries, eventual consistency — every subscriber has to manage.

Event buses, rules, targets, and patterns

coreintermediate

An event bus receives events and routes them. A rule watches the bus for events matching an event pattern (or a schedule) and sends matches to one or more targets. An archive stores matched events so you can replay them later.

Think of it as

The event bus is a mail room; a rule is a standing instruction like "any package matching this label, forward to these five desks." An archive is a copy of every matching package kept in storage, so you can resend the batch later without the sender resending anything.

What we're doing: Create a rule that matches a domain event and fans it out to two targets.

order-placed-rule.jsonjson
{
  "source": ["com.example.orders"],
  "detail-type": ["OrderPlaced"],
  "detail": { "amount": [{ "numeric": [">", 0] }] }
}
// aws events put-rule --name order-placed --event-pattern file://order-placed-rule.json
// aws events put-targets --rule order-placed --targets \
//   "Id"="1","Arn"="arn:aws:lambda:...:function:send-confirmation" \
//   "Id"="2","Arn"="arn:aws:sqs:...:queue/fulfillment-queue"
2
"source" and "detail-type" narrow the match to one producer and one event type, before "detail" filters on the payload itself.
4
A numeric matcher on "amount" only matches events where the field satisfies the comparison — not every OrderPlaced event.

Why this works: Both targets receive the same matching event independently and run in parallel — the Lambda function and the SQS queue do not know about each other, and a failure in one does not block delivery to the other.

Writing a pattern that is broader than intended, so unrelated events reach the target

Wrong

json
{ "source": ["com.example.orders"] }

Better

json
{ "source": ["com.example.orders"], "detail-type": ["OrderPlaced"] }

What you see: A target built to handle one event type silently receives every event type the source ever emits — OrderPlaced, OrderCancelled, OrderRefunded — and either errors on the unexpected shapes or, worse, processes them incorrectly without erroring at all.

Why: An event pattern matches on whatever fields you specify and ignores the rest — omitting "detail-type" means any event from that source matches, regardless of what kind of event it is.

An event's path from source to target
PutEventsevaluatedagainstmatched → invoke(up to 5, parallel)matchingevents copied

Event source

Event bus

Rule (pattern or schedule)

Target(s)

Archive (optional)

  • Event source
    • leads to Event bus (PutEvents)
  • Event bus
    • leads to Rule (pattern or schedule) (evaluated against)
    • leads to Archive (optional) (matching events copied)
  • Rule (pattern or schedule)
    • leads to Target(s) (matched → invoke (up to 5, parallel))
  • Target(s)
  • Archive (optional)

Remember: Bus routes, rule matches (pattern or schedule) and fans out to up to 5 parallel targets, archive stores a copy for replay back to the same source bus. Cross-account needs a resource policy on the receiving bus plus a rule on the sending side.

See also: domain events vs commands · loosely coupled event driven design

Domain events vs commands

standardintermediate

A domain event says something already happened ("OrderPlaced") and the sender does not know or care who reacts. A command tells one specific receiver to do something ("ChargeCard") and expects it to happen.

Think of it as

An event is a public announcement — the sender broadcasts it and moves on, with no idea who is listening or how many. A command is a direct instruction to one named recipient, who is expected to either do it or report back why not.

json
// Domain event — already happened, broadcast, no reply expected
{ "detail-type": "OrderPlaced", "detail": { "orderId": "o-123", "amount": 4200 } }

// Command — an instruction to one receiver, expects a direct outcome
{ "detail-type": "ChargeCard", "detail": { "customerId": "c-9", "amount": 4200 } }

Remember: Event = "this already happened," broadcast, any number of listeners. Command = "do this," one intended handler, an expected outcome. EventBridge's pattern-matched fan-out fits events; a direct call or targeted queue fits commands.

See also: event buses rules and targets · loosely coupled event driven design

Designing loosely coupled event-driven services

coreadvanced

Loosely coupled means a producer and its subscribers can change and deploy independently, because nothing depends on the other's internals. Event-driven systems buy that independence at the cost of four problems every subscriber has to handle: the event shape can change, the same event can arrive more than once, a failed delivery gets retried, and readers see stale data until the event catches up.

Think of it as

A producer publishing an event is like a radio broadcast, not a phone call — it does not know who is listening, cannot confirm they understood, and will repeat the broadcast if it thinks the first one did not land. Every subscriber has to be built for a broadcast medium: tolerate a changed format, tolerate hearing it twice, and tolerate a delay before its own picture of the world catches up.

What we're doing: Make an OrderPlaced handler safe against duplicate delivery.

handle_order_placed.pypython
def handle_order_placed(event):
    event_id = event["id"]

    if already_processed(event_id):
        return  # duplicate delivery — skip, do not re-charge or re-ship

    charge_card(event["detail"]["orderId"], event["detail"]["amount"])
    mark_processed(event_id)
2
EventBridge assigns every event a unique "id" — that ID, not the event content, is the key for deduplication.
4
Checking a durable store of processed IDs before acting is what makes a retry or a redelivered event a no-op instead of a double charge.
8
Marking the event processed only after the side effect succeeds means a crash between the charge and this line causes a safe re-attempt, not a lost record of the charge.

Why this works: At-least-once delivery means this handler will eventually receive the same event twice — building the dedup check in from the start is cheaper than debugging a double-charge in production later.

Treating a retried delivery as a new, independent event

Wrong

python
def handle_order_placed(event):
    charge_card(event["detail"]["orderId"], event["detail"]["amount"])

Better

python
def handle_order_placed(event):
    if already_processed(event["id"]):
        return
    charge_card(event["detail"]["orderId"], event["detail"]["amount"])
    mark_processed(event["id"])

What you see: A customer is charged twice for one order after a target briefly failed to acknowledge on the first attempt and EventBridge retried — the retry succeeded, so nothing in the logs even looks like an error.

Why: A retry is EventBridge doing exactly what its retry policy is documented to do — the bug is not in EventBridge, it is in a handler that assumed every invocation represents a distinct event instead of checking first.

Tightly coupled call vs loosely coupled event

Direct call (tightly coupled)

  • +Caller knows the receiver and blocks for its response
  • +A receiver's outage is the caller's outage too
  • +Changing the receiver's contract requires coordinating both sides at once

Event via EventBridge (loosely coupled)

  • Producer publishes without knowing which — or how many — subscribers exist
  • A subscriber outage delays that subscriber only; retries and a DLQ absorb it
  • New subscribers can be added later with zero producer changes
  • Direct call (tightly coupled)
    • Caller knows the receiver and blocks for its response
    • A receiver's outage is the caller's outage too
    • Changing the receiver's contract requires coordinating both sides at once
  • Event via EventBridge (loosely coupled)
    • Producer publishes without knowing which — or how many — subscribers exist
    • A subscriber outage delays that subscriber only; retries and a DLQ absorb it
    • New subscribers can be added later with zero producer changes

Remember: Four things every subscriber has to handle, not the platform: the event shape changes (add fields additively, keep old ones until migration completes), delivery is at-least-once (dedup on the event ID), failed targets retry automatically then hit a DLQ, and a subscriber's copy of the world lags the source until its next processed event.

See also: event buses rules and targets · domain events vs commands

Advertisement