Article

Designing an asynchronous boundary that survives outages

A team case study in replacing a brittle synchronous integration with durable intent, idempotent delivery, explicit recovery, and operational evidence.

A successful HTTP response is weak evidence for a successful business process.

On one enterprise platform, a core domain service handed critical work to a downstream system. The original path coupled their availability: a slow dependency delayed requests, an outage interrupted processing, and partial failures left several teams reconstructing intent from logs and database rows. It worked most days. That was precisely why it was dangerous.

Developers, QA engineers, architects, and operators replaced that failure chain with a durable asynchronous boundary. I led the technical design work: mapping failure states, defining the delivery contract, and turning the switchover into reversible release steps. Other members of the team brought the production history, test strategy, operational constraints, and business decisions the design needed.

During the periods we compared, tracked integration-related downtime fell by roughly 40 percent, and the core services observed better than 99.999 percent availability. Those are operational case-study measurements, not a controlled benchmark. The architecture was one part of the result.

The organization and system details are intentionally generalized. The decisions, collaboration patterns, and production lessons are real. This case study walks through the failure model, outbox and idempotency design, rollout, and operational evidence so you can apply the same reasoning to your own service boundary.

Replacing a synchronous failure chain

Open in Studio
Play the diagram to follow a downstream outage through durable retry and recovery.

The team started with the failure model

The first design conversation was not “Which broker should we use?” It was “What can fail after we accept responsibility for this work?”

We worked through the uncomfortable cases together:

  • the domain transaction commits and the process dies before publishing
  • the remote system accepts a request but our client times out
  • the remote system is unavailable for hours
  • a deploy interrupts an in-flight delivery
  • a poison message retries forever and hides healthy work
  • two workers deliver the same logical message
  • an operator needs to explain one customer’s state without reading source code.

That exercise turned a vague modernization request into functional and non-functional requirements. It also prevented “add a queue” from masquerading as architecture.

The queue was an implementation choice. The design problem was preserving responsibility across failures.

We made intent durable at the source

The pivotal decision was to commit the domain change and its outbound intent in the same database transaction. If the transaction succeeded, the work existed. If it rolled back, the work did not exist. A relay could fail repeatedly without losing the original decision.

The group compared several options before choosing a transactional outbox. When a service already owns a relational database, this pattern provides a local atomic guarantee without pretending that two independent systems share a transaction.

The relay used leases, retries with bounded backoff, dead-letter handling, and generation-based fencing. Every delivery carried a stable idempotency key. The receiving side treated duplicates as normal because timeouts make at-least-once delivery unavoidable.

A sanitized event contract had the fields support and both services needed:

{
  "id": "evt_01J...",
  "type": "WorkAccepted",
  "subjectId": "work_4821",
  "subjectVersion": 7,
  "occurredAt": "2026-06-09T14:22:31.000Z",
  "correlationId": "req_9138",
  "data": { "requestedAction": "synchronize" }
}

The receiver stored id before applying the effect. If the same event arrived again, it returned the earlier result. subjectVersion prevented an older event from overwriting newer business state, and correlationId connected the original request, outbox row, delivery attempts, and receiver logs.

The timeout path was explicit. The source committed at 14:22:31. The relay attempted delivery at 14:22:32 and timed out three seconds later. The row returned to pending with its next attempt time. If the receiver had accepted the first request, the retry found the stored event id and did no work. If it had not, the retry applied the effect. Support could explain either outcome from the delivery and deduplication records.

We separated transport from business progress

A message broker can reliably accept and redeliver bytes, but its acknowledgement says only that the transport handled a message. It cannot decide whether inventory was reserved, a payment remains valid, or the overall business transition is complete. Those decisions belong in application state where the owning domain can validate them and support can inspect their progress.

We kept business progress in explicit state owned by the application. Transport acknowledgements answered “Did the message move?” Domain state answered “Did the business process reach a valid outcome?” This distinction made retries safer and support conversations much clearer.

During design reviews, we repeatedly tested whether each handler had one bounded responsibility. Each handler recorded its result and emitted the next durable fact when necessary. Small handlers gave the team useful retry boundaries and kept a slow dependency from monopolizing a worker.

Operations was part of the design

The architecture was not production-ready until the team could answer these questions quickly:

  • How old is the oldest pending item?
  • Which partition or customer is falling behind?
  • How many deliveries are retrying, and why?
  • Which messages reached dead letter?
  • Is the downstream dependency slow, unavailable, or rejecting our contract?
  • Can we replay one item safely?
  • Can we prove whether a customer-impacting action happened?

Developers, QA, and operations defined the signals together: structured logs with correlation identifiers, distributed traces, health checks, service-level dashboards, and alerts tied to backlog age and failure behavior. We used release walkthroughs and staged failure exercises to teach the whole team how to diagnose the system. Observability became part of the acceptance criteria rather than a ticket for later.

The release plan mattered as much as the code

The switchover crossed multiple systems, so we decomposed it into reversible phases:

  1. Deploy the schema and dormant processing path.
  2. Write durable intent while the existing integration remains authoritative.
  3. Compare outputs and reconcile differences.
  4. Enable delivery for a controlled cohort.
  5. Expand traffic while watching lag, retries, and downstream outcomes.
  6. Keep a tested rollback path until the new route proves stable.
  7. Remove the old path only after the evidence says it is safe.

The phased plan gave developers, QA, operations, and stakeholders the same mental model. Each group helped define the evidence it needed, which made go/no-go decisions concrete. Confidence came from observable gates rather than optimism or seniority.

A pattern another team can reuse

The technology was useful, but the durable boundary was only one part of the result. The larger win came from connecting architecture, delivery, and operations:

  • model failures before choosing infrastructure
  • commit intent beside the state change that creates it
  • assume duplicates and make consequences idempotent
  • keep domain progress outside the transport
  • design support and recovery paths before launch
  • migrate in observable, reversible increments.

This approach is useful whenever a synchronous dependency turns one system’s outage into another system’s outage.

Asynchrony adds state and operational responsibility. It does not make complexity disappear.

When the business process matters, that is usually an honest trade.

Run the failure-modeling exercise before anyone picks a broker. Developers contribute implementation constraints, QA identifies untested transitions, operations brings recovery experience, architects protect system boundaries, and stakeholders clarify which outcomes matter. No one person holds all of that. The boundary survives an outage only when intent, idempotency, and recovery get the same attention as the happy path.