How to Build Change Feeds Across Regions

March 17, 2027 · 13 min read

Solutions Architect Pro · SAP-C02 · part of The Exam Room

The situation

The order-management service writes to a DynamoDB table orders in eu-west-1 at a steady ~400 writes/sec. Downstream consumers:

  • Warehouse service in us-east-1: needs every new order to trigger picking logistics. Delay tolerance: seconds.
  • Analytics pipeline in ap-southeast-2: ingests into a data warehouse for reporting. Delay tolerance: minutes.
  • Audit log in eu-west-1: needs every state change for compliance. Delay tolerance: immediate.
  • Notifications in eu-west-1: customer-facing status updates. Delay tolerance: seconds.

Today, warehouse and analytics poll the Region-local orders table (a Global Table replica). Polling works but: it’s inefficient (scans every N seconds), it misses rapid state changes (value A -> B -> C becomes “A then C”), and the warehouse team has a 30-second lag they can’t reduce.

The fix is change feeds. Every write generates an event; consumers in any Region subscribe to the feed; they see every mutation in order.

What actually matters

The core trade in change-data-capture is ordering guarantees in exchange for throughput flexibility. Strict per-key ordering scales horizontally but is single-consumer-per-shard. Fan-out-friendly buses scale to many consumers but typically require explicit ordering semantics.

The first thing to ask is: per-item ordering or global ordering? For orders, per-item ordering matters (order X’s status updates must arrive in sequence) but global ordering across orders doesn’t, customer A’s order and customer B’s order don’t need to be strictly interleaved. Per-key ordering maps cleanly to a sharded stream keyed by the primary key.

The second is: exactly-once or at-least-once? Most systems deliver at-least-once; consumers must handle duplicates idempotently. For analytics, duplicates are fine with deduplication; for warehouse picking, idempotent operations on order state handle duplicates.

The third is: how do we ship events cross-Region? Native change feeds tend to be per-Region. To deliver to consumers in two other Regions, something has to read from the source stream and publish in the consumer Region, either a relay function that publishes to a stream in each consumer Region, or a cross-Region pull (higher latency, higher cost).

The fourth is: data replication vs change replication. Replicating the table replicates the current state; replicating the change feed replicates the events. Consumers that need to look up state can use a local replica. Consumers that need to react to changes need the event overlay. Most cross-Region consumers need both.

The fifth is: backpressure and replay. Consumers fall behind; native change feeds typically retain events for a day or so, with longer retention available on stream services and indefinite retention only when the events are archived to object storage. The replay source needs to match the worst-case lag the consumer might develop.

What we’ll filter on

  1. Per-key ordering, does the system guarantee consumer sees changes in order for a given item?
  2. Fan-out, how many independent consumers can subscribe?
  3. Cross-Region delivery, how do events reach consumers in other Regions?
  4. Retention, how long can a consumer lag before events are lost?
  5. Cost profile, per-event, per-shard-hour, per-Region data transfer?

The change-feed landscape

  1. DynamoDB Streams. Native change-data-capture for DynamoDB. Per-item ordering via shards (one shard per partition). Consumers are Lambda (event-source-mapping) or Kinesis Client Library (KCL) readers. Retention 24 hours. Only accessible in the table’s Region.

  2. DynamoDB Global Tables. Multi-master replication between tables in different Regions. The data replicates; consumers in each Region see a local replica. Does not surface changes as events, if consumers want to react, they still need Streams on their replica (each replica has its own stream).

  3. Kinesis Data Streams + Lambda pipe from DDB Streams. Shim that reads from DynamoDB Streams and publishes to Kinesis, giving longer retention, more consumers, and cleaner ops. Standard pattern for fan-out.

  4. Amazon EventBridge. Event bus with rules-based routing. DynamoDB can emit to EventBridge via a Lambda bridge. Cross-Region EventBridge bus-to-bus forwarding is native. Best for heterogeneous event sources.

  5. Amazon MSK (Managed Kafka). Kafka as the event spine; higher operational weight; rich ecosystem. Appropriate when the organisation’s broader event architecture is already Kafka.

  6. Amazon DynamoDB Streams Kinesis Adapter / Kinesis data stream integration. DynamoDB can publish directly to a Kinesis Data Stream (in the same Region), bypassing the DynamoDB Streams + relay approach. Cleaner architecture where supported.

Side by side

Option Per-key order Fan-out Cross-Region Retention Cost
DDB Streams ✓ (per shard) Limited consumers Same Region only 24h Per-read
DDB Global Tables Per replica No event feed Per replica Replication + storage
KDS + Lambda relay ✓ (per shard) Many Manual relay 24h-365d Shard-hour + PUT
EventBridge + bus forwarding ✗ (unordered) Many rules ✓ (native) N/A (event-driven) Per event
MSK ✓ (per partition) Many MirrorMaker Topic policy Cluster + storage
DDB -> KDS direct Many Manual relay KDS retention Per-event + KDS

For orders with multiple cross-Region consumers, strict per-order ordering, and different latency profiles: DynamoDB Streams into Kinesis Data Streams, with Lambda relays publishing to Kinesis streams in the consumer Regions.

The cross-Region streaming architecture

eu-west-1 (primary) DynamoDB table: orders ~400 writes/sec Streams enabled (NEW_AND_OLD_IMAGES) Global Table replicas: us-east-1, ap-southeast-2 (data replicated; consumers still need stream for events) Kinesis Data Stream: orders-events-eu 8 shards, 7-day retention partition key: orderId Lambda: relay-to-us reads eu KDS writes us KDS batch 100 / 1s Lambda: relay-to-ap reads eu KDS writes ap KDS Lambda: audit-log reads eu KDS -> writes immutable audit store S3 with Object Lock Compliance S3: orders-events-archive Kinesis Data Firehose to S3 (1-min batches) 90-day replay window; partitioned by event-time us-east-1 (warehouse) KDS: orders-events-us 4 shards, 24h retention consumer: warehouse service Lambda: warehouse-reactor reads KDS updates warehouse state p99 event-to-reaction: <2s Global Table replica orders table (read) used for state lookups Warehouse service picking, packing idempotent on orderId handles replay from archive if consumer lags > 24h ap-southeast-2 (analytics) KDS: orders-events-ap 2 shards, 24h retention Kinesis Firehose 5-min buffers -> Parquet to S3 -> Redshift COPY Redshift data warehouse analytics dashboards
DynamoDB Streams feed a central Kinesis Data Stream in the primary Region; Lambda relays replicate the stream cross-Region; each consumer Region has its own Kinesis + Lambda consumers; S3 archive provides long-term replay.

The picks in depth

DynamoDB Streams on the primary table. Streams enabled with NEW_AND_OLD_IMAGES, consumers see both the pre-change and post-change item. Alternative NEW_IMAGE is smaller but doesn’t tell downstream what changed; for state reconciliation, NEW_AND_OLD_IMAGES is worth the extra bytes.

Streams provide per-item ordering: all changes for a given order ID appear in a specific shard and are processed in order by that shard’s consumer.

Bridge to Kinesis Data Streams. Two options:

  • DynamoDB’s built-in Kinesis integration (newer, preferred): table publishes directly to a specified Kinesis stream in the same Region. No Lambda bridge required.
  • Lambda event-source-mapping from DynamoDB Streams reading and re-publishing to Kinesis: older pattern, still works, more operational knobs.

Either way, the Kinesis Data Stream becomes the canonical event spine. Kinesis has 7-day retention (and up to 365 with extended retention for a cost), larger than DynamoDB Streams’ 24 hours; multiple concurrent consumers (where DDB Streams has limits); better fan-out.

Per-Region Kinesis streams with Lambda relays. In the primary Region, a Lambda (one per target Region) consumes the orders-events-eu stream and publishes to orders-events-us or orders-events-ap. The relay preserves the partition key (orderId) so per-order ordering is maintained in the destination stream.

Relay considerations:

  • Batch size and max wait tuned for latency budget. Batch of 100 or 1 second means at most 1-2 seconds of latency on the relay hop plus Kinesis put latency.
  • Error handling: on destination-Region throttling, Lambda retries with backoff; the event source mapping’s default behaviour handles this. Persistent failures go to a DLQ for investigation.
  • Cross-Region data transfer priced per GB; for 400 events/sec with 1KB events, ~30GB/day = ~$600/month of inter-Region transfer per destination, not nothing.

Per-Region consumers. In us-east-1, Lambda functions subscribe to orders-events-us. Each Lambda is idempotent on orderId + eventTime, two deliveries of the same event result in the same state. The warehouse service receives events via SQS or directly from the Lambda.

In ap-southeast-2, analytics uses Kinesis Data Firehose to buffer events into Parquet and write to S3, then Redshift COPY for reporting.

Archive for replay. Firehose in the primary Region subscribes to the central Kinesis stream and writes all events to S3 partitioned by event time. Retention 90 days (configurable). If a consumer lags beyond Kinesis retention, it can be reset by replaying from the S3 archive, a Lambda reads the archive for a time range and re-publishes to the consumer’s Kinesis stream.

Ordering guarantees end-to-end. The chain DynamoDB Streams shard -> Kinesis shard -> relay Lambda -> consumer Region Kinesis shard -> consumer Lambda must preserve the partition key at each step. Any step that aggregates multiple shards into one (e.g., a Lambda that reads all shards and republishes) breaks ordering. The pattern has to be per-shard lineage: each Kinesis shard’s events land in a matching shard in the destination.

Cost model.

  • Kinesis shards: $0.015/shard/hour + per-PUT charges. For 8 shards in eu + 4 in us + 2 in ap = 14 shards = ~$150/month.
  • Lambda invocations: modest for 400/sec with batching.
  • Cross-Region data transfer: the dominant cost, $0.02/GB out of eu to other Regions; ~$600/month per consumer Region for 30GB/day.
  • DynamoDB Streams: priced on stream read operations; modest.

Total: ~$1500/month for the streaming infrastructure, plus the consumer-side compute. Cheap compared to losing orders because polling missed a state transition.

A worked event flow

Customer places an order at 14:22:07 UTC. orders table gets a Put. The full life:

  1. 14:22:07.100. DynamoDB Streams records the change; visible in the stream at 14:22:07.200.
  2. 14:22:07.250. DynamoDB Kinesis integration publishes the event to orders-events-eu Kinesis stream.
  3. 14:22:07.400 – relay-to-us Lambda reads the event in its next batch (batch window 1s). Publishes to orders-events-us Kinesis stream at 14:22:08.450 (allowing for cross-Region propagation).
  4. 14:22:08.600 – warehouse-reactor Lambda in us-east-1 reads the event. Processes, updates warehouse state at 14:22:09.
  5. End-to-end latency eu write to us reaction: ~2 seconds. Within the warehouse’s seconds-latency target.

For ap-southeast-2 with Firehose buffering at 5 minutes: same first two hops, then Firehose buffers until 14:27, then S3 write, then Redshift COPY on an hourly schedule. Analytics sees the event in the warehouse within the hour, which is well under the minutes-to-hours tolerance for analytics.

What’s worth remembering

  1. Global Tables replicate data; Streams replicate events. For consumers that need to react, Global Tables aren’t enough, each replica has its own stream.
  2. DynamoDB’s direct Kinesis integration is the modern spine. Cleaner than bridging through DynamoDB Streams + Lambda. Publishes directly to a specified Kinesis stream.
  3. Per-key partitioning preserves ordering. Use the primary key as the Kinesis partition key; every step downstream must preserve it.
  4. Lambda relays handle cross-Region replication. Kinesis doesn’t natively replicate across Regions; a small Lambda reading the source and publishing to the destination is the pattern.
  5. Kinesis retention defaults to 24h; S3 archive for longer replay. For consumers that might fall behind, Firehose to S3 is the safety net; replay tools read the archive and re-publish.
  6. Each consumer Region has its own stream. Centralising all consumers on one Region’s stream creates cross-Region read latency for every consumer; local streams give local latency.
  7. Idempotency is the consumer’s job. At-least-once delivery is the default; consumers must dedupe or apply idempotent logic based on event ID + time.
  8. Cross-Region data transfer cost dominates. Per-GB egress adds up. Architect to minimise cross-Region bytes (e.g., compress events, filter at source).

One change, many consumers, three Regions. The streaming overlay on top of Global Tables gives every region both the current state and the history of how it got there, in order, durably, within the latency budget of the consumer that reads it.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.