Software design and patterns
Design patterns in practice: responsibility, state and trade-offs
By the BackendDrills editorial team · Published and checked October 6, 2026 · 6-minute read
Before you start: Code responsibilities and unit testing. Find this reading in a study path →
A useful pattern isolates a responsibility that changes for a reason. Name that reason before introducing an interface. The following exercises use Java examples, but the ownership and contract questions apply across languages.
Strategy without shared request state
Imagine a delivery quote service with local and international policies. Each policy computes a quote from explicit inputs. The policy object does not need to remember the current customer or the last route. Putting those values into mutable fields on a shared service makes concurrent calls interfere even though each policy's arithmetic is correct.
// Illustrative signature; policy is selected for this request.
Quote quote(Shipment shipment, PricingPolicy policy);
// Inputs carry request data. The policy expresses a decision rule.
Use two overlapping requests with different routes. Assert each result against its own inputs, then reverse completion order. If adding synchronization seems necessary, first ask whether the mutable state belongs in the shared object at all. Spring's default singleton bean scope provides a shared bean instance within the relevant container; it does not turn mutable request fields into isolated state.
A familiar strategy: ordering
A comparator supplies an ordering rule independently of the object being sorted. Its contract still matters: inconsistent or non-transitive comparisons can invalidate the result. For a priority queue of work, write tie and null policies explicitly. An abstraction does not remove the requirement to define observable behavior.
Adapter without losing meaning
A provider returns “accepted” when it has queued work. Your domain API promises “completed” only after an effect is confirmed. An adapter that maps both to a generic success value hides the difference. Keep provider acceptance, pending work, confirmed completion and definitive rejection distinct where the domain depends on them. Record which provider state or lookup establishes completion.
| Design question | Review artifact |
|---|---|
| What varies? | A concrete upcoming change and its owner. |
| What remains invariant? | A contract every implementation must satisfy. |
| Where does state belong? | Request, shared process or durable store. |
| What is the cost? | Extra indirection, configuration and testing surface. |
Try a small redesign
Take a branching function with two policies. Implement one interface, keep request inputs explicit and write common contract tests for both implementations. Then describe a plausible third policy. If adding it still requires edits throughout unrelated code, revisit the boundary. If no meaningful variation exists, a straightforward function may be easier to understand. Finish with the alternative you rejected and the condition that would make you reconsider.
Check the underlying behavior
Original illustrative examples, prepared with AI assistance and checked against the linked primary documentation. No customer incident or vendor endorsement is claimed. Our editorial approach.
Practice the next decision
Try a complete free backend drill: inspect evidence, make three decisions and review the reasoning. No account or card required.
Try a free incident drill →Selected practice from the study paths
- Strategy selection leaks between requests · Full edition drill
- A modular monolith becomes a distributed monolith · Full edition drill
Free readings need no account. Full edition drills require verified access; opening a paid link does not expose its answers.