Jul 11, 2026 · 10 min read
Use Aurora DSQL CDC to Transport an Outbox, Not to Invent Domain Events
Aurora DSQL's managed CDC removes the connector, not the outbox. Why intentional, versioned outbox rows beat exporting raw row mutations as domain events.
Use Aurora DSQL CDC to Transport an Outbox, Not to Invent Domain Events
Aurora DSQL’s native delivery into Kinesis removes a real piece of infrastructure, but it does not turn arbitrary row changes into a trustworthy event contract. Delivery stays at-least-once and explicitly unordered, so the application still owns identity, business meaning, ordering, replay, and failure recovery.
My rule for it is narrow. I would use Aurora DSQL CDC to transport append-only transactional outbox rows, and I would not expose ordinary table mutations as domain events unless the consumer genuinely wants a disposable, current-state projection and can live with the source schema as its contract.
The Managed Part Stops at Transport
On May 14, 2026, AWS announced change data capture for Amazon Aurora DSQL in public preview. It sends committed row-level inserts, updates, and deletes as JSON directly to Amazon Kinesis Data Streams. The listed uses cover microservice synchronization, Lambda workflows, search indexing, and analytics.
The operational win is genuine. There is no separate CDC connector to deploy, patch, scale, monitor, or recover, and there are fewer credentials and network paths to manage. The database service owns extraction from its internal change history and delivery toward Kinesis.
That fact is also the strongest argument against keeping a transactional outbox in front of Kinesis. If the database can already publish every committed change, why write a second row describing the same transaction?
The answer is that the second row isn’t a transport workaround. It is where the application commits to business meaning. A change to orders.status from pending to cancelled is a database fact; OrderCancelled is a domain statement with a stable identity, an expected payload, a schema version, and rules about who may react to it. The two often happen in the same transaction, but they are not interchangeable. A later migration might replace the status column, split the table, or add an intermediate state without touching the domain event at all.
So native CDC eliminates connector operations while leaving the semantic boundary between storage and integration exactly where it was. The Aurora DSQL CDC delivery contract sharpens that point. Delivery is at-least-once, records are explicitly unordered, changes to the same primary key can arrive out of commit order, and records from one transaction may be interleaved with records from others. Those are workable transport semantics and poor implicit domain semantics.
Raw Row Changes Export the Storage Model
Say cancelling an order updates an orders row, releases several reservation rows, and inserts an audit record, all in one transaction. A raw CDC consumer sees row mutations and has to infer whether they represent a cancellation, an administrative correction, a retry, or a migration. Transaction metadata can tell it which mutations committed together, but it cannot supply the business intent the application never wrote down.
The cost grows with every new consumer. The search indexer learns one interpretation of orders.status, the cache invalidator learns another, and a workflow service ends up watching both orders and reservations because neither table alone is enough. From then on, every schema change drags an undocumented integration review behind it.
Missing ordering guarantees make the inference less reliable still. Picture the same order moving through versions 17, 18, and 19. If the records arrive as 19, 17, 18, a consumer that applies them in arrival order lands on the wrong state, and Kinesis shard ordering does nothing to repair disorder that already existed when the records reached the shard.
Deletes are the sharper edge. If a delete arrives before an older update, a naïve projection can resurrect a record that no longer exists. To prevent that, the consumer has to keep a tombstone carrying enough ordering information to reject the stale change; simply removing the projected document throws away the evidence it needs.
None of this rules out raw changes when the storage model is deliberately the contract. A disposable current-state mirror can compare commit metadata, retain tombstones, and rebuild from the source. Analytics ingestion can normalize duplicates and sort things out later. Neither case has to pretend a row update is a durable business event. Once the change drives billing, notifications, entitlement changes, cache invalidation, or any other irreversible workflow, inference becomes the wrong boundary to build on.
An Append-Only Outbox Makes Intent Explicit
The safer design writes the domain change and an outbox row in the same Aurora DSQL transaction, then lets CDC carry the committed outbox insertion into Kinesis. There is no polling publisher and no separately operated connector, yet the event still has an application-owned contract behind it.
I would keep the outbox append-only and give each row at least an immutable event_id, the aggregate type and identifier, an event type, an aggregate version, a payload schema version, an occurrence timestamp, and the payload. Aggregate version and payload schema version answer different questions. One orders changes to a business entity; the other governs how consumers decode the event.
The payload should describe the event rather than photocopy every column of the source table. An OrderCancelled event might carry the order identifier, cancellation reason, actor classification, and version, and it should never force consumers to rebuild meaning by joining three mutable tables after delivery.
An outbox also makes schema evolution reviewable. A database migration can stay an internal concern, while changing an event’s payload or meaning forces a new schema version and a deliberate compatibility decision. That work always existed; raw CDC just hid it inside consumers.
Cleanup needs an explicit rule, because deleting old outbox rows produces its own delete records. The transport adapter has to tell those maintenance tombstones apart from domain events, and it should normally publish only outbox insertions as business messages. An intentional CustomerDeleted belongs in an inserted row; deleting that row six months later is storage maintenance, not a second customer deletion.
Retention has to match the replay requirement. Prune rows quickly and you get a short replay window; if a long-lived event history matters, retain or archive accordingly. Aurora DSQL’s undelivered-change retention is a transport recovery boundary, not an event archive, and treating it as one will disappoint you during an incident. The real payoff of an outbox in front of Kinesis is simple: it hands native CDC one stable table to transport while keeping business meaning inside the transaction that created it.
Every Consumer Still Needs an Inbox and a Reconciliation Path
At-least-once processing means duplicates are routine, not exceptional. Each consumer should record event_id in an inbox or equivalent dedup store, and make that recording atomic with its local projection update wherever it can. For database-backed projections, a conditional insert on the event identifier usually does the job.
External side effects need something stronger. If the destination accepts an idempotency key, use event_id; if it doesn’t, persist a local state machine that separates pending, attempted, and completed work. Mark an event complete before the external call and you risk losing it, mark it complete after and you risk repeating it. Native CDC does not remove that dilemma.
Ordering has to be handled per effect, and the choices are distinct enough to spell out:
- A current-state projection can store the highest applied aggregate version and reject anything older.
- A projection built directly from row mutations can compare commit metadata and keep deletion tombstones.
- A consumer that needs every transition must detect version gaps, buffer within a bounded window, and reconcile missing events from retained history.
- A multi-row transactional projection must use transaction metadata or reread canonical state instead of assuming adjacent Kinesis records belong together.
Commit timestamps help reject stale writes, but they don’t create domain order; an explicit aggregate version is clearer when several events can touch the same entity. When one transaction writes two outbox rows, the application should decide whether their relative order matters rather than leaning on arrival order to guess for it.
Replay is a product decision too. You can reprocess retained Kinesis records, rebuild from the canonical database, or republish retained outbox rows to a separate replay stream. The last option is often safest because it keeps recovery traffic out of live delivery. Whatever a consumer supports, it should know before the first incident, including how it resets its inbox and keeps replayed external side effects from firing twice.
Aurora DSQL adds a failure mode that belongs directly in this plan. Kinesis throughput limits, oversized records, and IAM or KMS failures can push replication lag up. Aurora DSQL retains undelivered changes for one week, but a CDC stream that reaches FAILED cannot recover and must be recreated.
So the runbook cannot end with “restart the consumer.” It has to cover creating a replacement stream, identifying the affected interval, and reconciling that interval from the outbox or canonical state. Without a retained source of event intent, recreating the pipe proves nothing about whether downstream systems are complete. I would alert on stream state and Kinesis replication lag long before the one-week mark, with a threshold derived from demonstrated drain rate and recovery time and enough margin to fix IAM, KMS, capacity, or record-size problems and still clear the backlog. One week is a cliff, not an operating target.
Cache Invalidation and Search Must Degrade Independently
Cloudflare cache invalidation makes a good outbox consumer because the event can name the affected cache tags, keys, or resources. A raw row update usually can’t, not without duplicating routing and representation logic inside the invalidator.
It is still an asynchronous projection, which has consequences. A successful database commit must never depend on Cloudflare accepting a purge request. Duplicate invalidations should be harmless or coalesced, and the consumer should track the latest source position it has applied. Beyond that, the application needs a stated policy for lag. If invalidation falls behind and stale content is tolerable, keep serving it within the declared freshness window; for correctness-sensitive resources, bypass the affected cache and read from origin until the consumer catches up. That decision lives in the request path and operational policy, not in a silent assumption that Kinesis is always current.
Search projections deserve the same discipline. Search is derived state, so the index should be disposable: apply events idempotently, reject stale aggregate versions, and rebuild into a fresh index when reconciliation gets cheaper than patching gaps. Exact-identifier lookups and other critical reads should have a path to canonical data instead of quietly trusting a lagging index.
When Kinesis backpressure starts threatening the one-week retention window, promising fresh cache and search results only enlarges the incident. Degrade those projections first. Cut nonessential consumers if they’re competing for downstream capacity, bypass stale paths where correctness matters, and protect your ability to recover the event stream. This is exactly why lag has to be visible past the CDC operator. The API and edge layers need a coarse health signal telling them whether derived views are current enough for their purpose, because a green database sitting behind a badly delayed search index is not a healthy read path.
Raw CDC Is Useful When Mutations Really Are the Contract
An outbox costs something: write amplification, retained data, schema governance, and one more table to operate. I would not bolt it onto every Aurora DSQL workload out of habit.
When a single service owns both the table and a rebuildable projection, raw Aurora DSQL CDC is often the simpler choice. That consumer can treat updates as latest-state facts, keep commit-ordering metadata and tombstones, and rebuild whenever its assumptions shift. Exploratory analytics that normalizes duplicates and disorder downstream fits the same pattern.
The line moves the moment independent consumers depend on what a change means. If a mutation can start a workflow, call an external service, invalidate public content, or land in a durable audit trail, exporting the storage model saves one outbox write by scattering semantic reconstruction across every consumer that reads the stream.
Managed CDC improves the happy path a lot. Whether it lowers total system complexity depends entirely on what it carries. Transport intentional, versioned outbox rows and you drop connector operations while keeping a durable application contract. Transport arbitrary mutations and you drop visible plumbing while leaving ordering, idempotency, replay, degradation, and business meaning scattered through the rest of the system.
References
- AWS, “Amazon Aurora DSQL change data capture is now available in public preview”: https://aws.amazon.com/about-aws/whats-new/2026/05/amazon-aurora-dsql-change-data-capture-preview/
- Amazon Aurora DSQL User Guide, “Change data capture streams”: https://docs.aws.amazon.com/aurora-dsql/latest/userguide/cdc-streams.html
- Amazon Aurora DSQL User Guide, “Monitoring change data capture”: https://docs.aws.amazon.com/aurora-dsql/latest/userguide/cdc-monitoring.html