Back to Case Studies
AWS Backend Architecture

Event-Driven Document Synchronization on AWS

A failure-aware backend architecture using EventBridge, SQS, Lambda, PostgreSQL, S3, and OpenSearch—designed around reliable publication, idempotency, backpressure, status aggregation, and safe search visibility.

Role

Backend architect and AWS solution designer

Published

Scope

Production reference architecture

Depth

18 min read

Problem

A document upload had to remain fast while several downstream systems processed independently, without losing events, duplicating side effects, or exposing incomplete records in search.

Solution

I designed a transactional-outbox event pipeline with per-consumer queues, idempotent Lambda workers, version-aware projections, explicit readiness policies, reconciliation, and two distinct dead-letter paths.

Impact

The design contains partial failures, scales each integration independently, gives operators an explainable document state, and makes search visibility a deliberate consistency guarantee rather than a timing assumption.

Designing an Event-Driven Document Synchronization Platform on AWS

This is not a tutorial about connecting three AWS services. It is a system-design case study about preserving a fast upload experience while several independent systems validate, enrich, replicate, and index the same document.

The central question: how can an upload be accepted quickly without claiming the document is ready before every required downstream projection is trustworthy?

End-to-end event-driven document synchronization pipeline on AWS.

1. The operational problem—not the happy path

A synchronous implementation looks attractive: save metadata, call every downstream API, update search, then respond. It also couples upload latency and availability to the slowest dependency. One timeout can leave the caller uncertain even when part of the work succeeded, and retries can duplicate side effects.

1

Uploads must remain responsive

Acknowledge after durable acceptance, not after every integration finishes

2

Consumers run at different speeds

Give each integration its own queue and concurrency controls

3

Failures must be diagnosable

Track status, attempts, error category, and timestamps per destination

4

Search must not expose partial data

Publish to the searchable read model only after the readiness policy passes

5

Retries are unavoidable

Treat delivery as at least once and make every consumer idempotent

6

New integrations will be added

Route versioned domain events without changing the upload API

2. Architecture and responsibility boundaries

End-to-end document synchronization

Client
API Gateway / Upload API
S3 object storage
PostgreSQL: document + outbox
Outbox publisher
Amazon EventBridge
Validation SQS
Enrichment SQS
Archive SQS
Search SQS
Lambda consumers
Per-target sync transactions
Readiness aggregator
OpenSearch searchable projection

EventBridge handles routing; SQS owns buffering and backpressure; Lambda runs stateless workers; PostgreSQL remains the source of truth; OpenSearch is a derived read model. Those boundaries matter because an event bus is not a work queue, and a search index should not become the authority for workflow state.

This complements my enterprise document pipeline monitor, which focuses on secure storage operations and production observability. Here, the emphasis is the backend coordination model behind the pipeline.

3. Closing the database-to-event reliability gap

Saving a document row and calling PutEvents are two separate writes. If the database commits and event publication fails, the document is stranded. If the event is published and the transaction rolls back, consumers act on state that does not exist.

I would use a transactional outbox: write the document, required synchronization targets, and an outbox event in one PostgreSQL transaction. A separate publisher claims unpublished rows, sends them to EventBridge, and records the publication result. AWS documents this exact dual-write risk in its transactional outbox guidance.

BEGIN
Insert document
Insert target transactions
Insert outbox event
COMMIT
Publish asynchronously

The API can now return 202 Accepted with a document ID and status URL after durable acceptance. The client receives an honest promise: the work was recorded, not that every projection is already complete.

4. Event contract design

Events carry stable identifiers and routing facts—not the document binary or an accidental copy of the database model.

{
  "specversion": "1.0",
  "id": "evt_01J...",
  "type": "document.accepted.v1",
  "source": "document-ingestion-api",
  "time": "2026-08-07T08:30:00Z",
  "data": {
    "documentId": "doc_01J...",
    "tenantId": "tenant_123",
    "objectKey": "tenant_123/doc_01J.../source.pdf",
    "contentType": "application/pdf",
    "correlationId": "req_01J..."
  }
}
  • Version event types explicitly so producers and consumers can evolve independently.
  • Include tenant, correlation, event, and document identifiers for authorization and tracing.
  • Keep sensitive metadata and presigned URLs out of long-lived events.
  • Store schemas in source control and run compatibility checks in CI.
  • Treat event ordering as scoped to a document, not globally guaranteed.

5. Why EventBridge and SQS are both present

1

EventBridge

Job
Content-based routing and fan-out
Failure it isolates
Producer does not know every consumer
2

One SQS queue per consumer

Job
Durable buffer and backpressure boundary
Failure it isolates
A slow search indexer does not slow validation
3

Lambda event source mapping

Job
Batch polling and elastic worker execution
Failure it isolates
Consumers scale according to their own workload
4

Consumer DLQ

Job
Quarantine poison messages
Failure it isolates
Repeated bad inputs stop consuming retry capacity

EventBridge target delivery failure and consumer processing failure are different operational events. An EventBridge DLQ captures events the bus could not deliver to a target; each consumer queue needs its own redrive policy for messages that arrived but could not be processed. AWS explains the former in its EventBridge DLQ documentation.

6. Idempotency: designing for at-least-once delivery

Duplicate delivery is normal. A Lambda can complete its side effect and time out before SQS receives the acknowledgement. The retry must become a no-op, not a second index entry or duplicate notification.

Receive event
Validate contract
Claim idempotency key
Perform side effect
Commit target status
Acknowledge

A unique key such as (event_id, consumer_name) prevents duplicate claims. Updates use compare-and-set semantics so a stale worker cannot overwrite a terminal success. For batches, partial batch responses retry only failed records; AWS also recommends idempotent Lambda consumers because SQS event source mappings process at least once in the Lambda and SQS guide.

7. The status model is a state machine

1

document

Key fields
id, tenant_id, object_key, overall_status, version
Purpose
Canonical lifecycle and optimistic concurrency
2

document_sync_target

Key fields
document_id, target, required, status, attempts, next_retry_at, error_code
Purpose
Independent truth for every projection
3

outbox_event

Key fields
event_id, aggregate_id, event_type, payload, published_at
Purpose
Reliable event publication
4

processed_event

Key fields
event_id, consumer, processed_at
Purpose
Idempotency ledger
5

document_status_history

Key fields
from_status, to_status, actor, reason, occurred_at
Purpose
Auditable transitions
ACCEPTEDPROCESSINGREADYDEGRADEDFAILEDQUARANTINED

The aggregator calculates readiness from required target rows in one transaction. It never trusts a worker-supplied “all done” flag. A document becomes READY only when every required projection has reached its terminal success state; optional enrichments can fail and produce DEGRADED without hiding an otherwise usable record.

8. Search visibility without partial records

Search indexing is a projection, not the final source of truth. The search consumer builds a complete document from canonical data, writes a deterministic index ID, and records the source document version. Queries filter on is_searchable=true and tenant scope. If an older event arrives late, version comparison prevents it from overwriting newer content.

  • Index with document ID as the stable OpenSearch _id.
  • Persist source_version and reject stale updates.
  • Use an alias for zero-downtime index migrations and reindexing.
  • Run reconciliation jobs that compare READY records with the search projection.
  • Delete through the same event path so search and downstream replicas converge.
Document lifecycle transition policy gate guarding search visibility.

9. Failure taxonomy and recovery

1

Transient

Response
Exponential backoff with jitter
Example
Downstream 503 or throttling
2

Permanent data error

Response
Quarantine immediately
Example
Unsupported file type or invalid schema
3

Poison message

Response
Move to consumer DLQ after bounded attempts
Example
Repeatable parser crash
4

Delivery failure

Response
Inspect EventBridge target DLQ
Example
Missing queue permission
5

Silent drift

Response
Detect and repair through reconciliation
Example
READY row missing from search

Redrive is an audited operation, not “send everything again.” An operator fixes the cause, selects messages by error category, records the reason, and replays them with the original event ID so idempotency remains intact.

10. Backpressure, scaling, and cost controls

Unlimited concurrency can turn a traffic spike into a downstream outage. Each queue gets a reserved concurrency budget based on dependency capacity. Batch size, batching window, visibility timeout, maximum receive count, and payload duration are tuned from measurements—not copied from defaults.

  • Scale on queue age as well as queue depth; age reveals whether the user-facing SLA is at risk.
  • Cap search-writer concurrency to protect the cluster during ingestion bursts.
  • Use S3 object references rather than placing document bodies in events.
  • Batch compatible writes, but preserve per-record failure reporting.
  • Set retention and log sampling policies so observability cost does not grow without bounds.

11. Security and tenant isolation

Authentication at the API is only the first boundary. Every message carries a tenant ID, but workers verify that the document and S3 key belong to that tenant before access. IAM roles are separated per consumer, S3 and SQS are encrypted, secrets remain in Secrets Manager, and personally identifiable metadata is excluded from logs and event payloads.

For the broader authorization model—permission composition, backend enforcement, cache invalidation, and auditability—see my enterprise RBAC architecture deep dive.

12. Observability tied to user impact

1

End-to-end readiness latency p50/p95/p99

How long until an accepted document is usable?

2

ApproximateAgeOfOldestMessage

Which consumer is threatening the processing SLA?

3

Failure and DLQ rate by target/error code

Is the issue systemic, data-specific, or permission-related?

4

Outbox unpublished age

Are committed documents failing to enter the event system?

5

Search reconciliation drift

Does user-visible search match canonical state?

6

Correlation trace

What happened to one document across every boundary?

CloudWatch dashboards are useful only when they lead to action. Alarms map to runbooks for stuck outbox rows, queue backlog, permission failures, throttling, and DLQ growth. User-facing notifications are emitted from state transitions; the delivery design is covered in my production notification system tutorial.

Cloud operations dashboard monitoring ingestion queues, error rates, and index sync drift.

13. Delivery strategy and verification

I would deliver one consumer end to end first, then add routes behind the same contract. Infrastructure is defined as code, deployments use aliases and gradual traffic shifting, and contract tests validate events before promotion.

Upload + outbox
One queue + consumer
Status aggregation
Search gate
Failure drills
Additional consumers
  • Load test burst traffic and verify backpressure protects dependencies.
  • Kill a worker after its side effect to prove retry idempotency.
  • Break queue permissions to prove EventBridge delivery alarms and DLQ capture.
  • Inject malformed documents to prove quarantine behavior.
  • Replay duplicate and out-of-order events to prove version guards.
  • Rebuild the search index from canonical records to prove recoverability.

14. Trade-offs and alternatives I considered

1

SNS + SQS

When I would choose it
Simple broadcast without rich routing
Why not the default here
EventBridge gives cleaner content-based routing and integration evolution
2

Step Functions

When I would choose it
A bounded workflow needs explicit sequencing or compensation
Why not the default here
Independent projections do not need central orchestration for every step
3

SQS FIFO

When I would choose it
Strict per-document ordering is mandatory
Why not the default here
Lower flexibility and throughput are unnecessary when consumers use version guards
4

DynamoDB + Streams

When I would choose it
The canonical model fits key-value access and CDC
Why not the default here
Relational transactions and operational queries fit this workflow well
5

Direct Lambda targets

When I would choose it
Very small, low-risk fan-out
Why not the default here
Queues provide the backpressure and failure isolation required here

15. Outcome: what this architecture proves

The result is a platform that acknowledges uploads quickly, contains downstream failures, exposes truthful progress, and prevents incomplete records from leaking into search. More importantly, it is designed around the uncomfortable realities of distributed systems: dual writes, duplicates, stale events, poison messages, partial failure, recovery, and cost.

That is the engineering strength this case study demonstrates: I do not begin with a list of AWS services. I begin with invariants, failure modes, data ownership, and operational outcomes—then choose the smallest set of services that makes those guarantees understandable and evolvable.

Names, volumes, identifiers, and domain-specific workflows are intentionally generalized to protect confidential implementation details.