intermediate
3 min answer
Multiple choice
A service calls a payment provider over HTTP. The provider occasionally returns 503 and occasionally times out. In a domain / application / adapter layering, which layer should own the retry and backoff policy?
Show the full answer Hide the answer
The deciding property
Put the policy in the layer that holds the fact the decision depends on. "A 503 is retryable and a 400 is not" is knowledge about HTTP and about this specific provider, and it exists in exactly one place: the adapter. "This operation must finish within 2 seconds" lives in the application layer. "A customer must never be charged twice" is a domain invariant, and it is satisfied by an idempotency key, not by a retry count.
Why the adapter, with two constraints from above
The adapter retries, but it is not sovereign:
- The deadline is passed in, not invented. The application layer knows the remaining budget for the whole use case; the adapter retries only within it. Without this, a three-attempt policy with exponential backoff can consume seven seconds inside a two-second use case and the caller has already given up.
- The port declares whether the operation is safe to retry. A retry of a non-idempotent call is not a transport concern, it is a correctness decision, and the port should force the caller to supply an idempotency key for anything that mutates.
The domain remains testable with no transport, which is the entire reason for the layering.
Why the other options fail
- The domain layer would have to know that HTTP has a 503 and that sockets have timeouts, which is precisely the dependency direction the architecture exists to forbid, and it would make the domain untestable without a network. The instinct behind this option is half right: "attempt this payment through a second acquirer if the first declines" genuinely is a business rule. But that is a compensation policy, not a retry, and the distinction is the point.
- The application layer is attractive and nearly right, because it owns the deadline. To run the retry loop it would have to interpret transport errors, which reintroduces exactly the coupling you removed. Give it the deadline and the outcome; keep the loop below it.
- All three is the production failure this question exists to prevent. Nested retries multiply: three attempts at each of three layers is twenty-seven requests against a provider that is already overloaded, which is how a degraded dependency becomes a dead one. Retry at exactly one layer, and make it the lowest one that knows what the error means.
What would flip the decision
| If this changes | Then | Because |
|---|---|---|
| The re-attempt uses different parameters or a different provider | It belongs in the use case | It is a business decision, not a transport recovery |
| The call is fire-and-forget with a durable queue | The queue owns redelivery | The retry outlives the process and cannot live in either layer |
| A service mesh already retries at the network layer | Remove the adapter's retry | Two layers retrying is the twenty-seven-request case with extra steps |
Common weak answers
"Retry everywhere, it is defensive" is the most common and the most damaging: it is not defence, it is amplification, and it is invisible until the dependency is already struggling.