Naming, Boundaries, and Error Contracts
Good names tell readers what a program means; good boundaries tell them where that meaning is enforced. An error contract connects the two by explaining what happened and what a caller can do next.
Consider a function named process that returns False for malformed input, insufficient stock, and a database timeout. Renaming it reserve_units helps, but its callers still cannot choose a sensible response. Improve the contract and responsibility boundary together, then make the name describe the resulting behavior.
TL;DR
- Name the domain operation, its units, and consequential side effects.
- Separate parsing, business decisions, persistence, and transport mapping when they have different reasons to change.
- Give expected rejections stable machine-readable meanings.
- Keep infrastructure failures distinguishable from ordinary business outcomes.
- Test callers’ recovery decisions as well as successful returns.
Quick Example
This complete pure Python example checks a fictional reservation calculation. Invalid requested quantity and insufficient stock have distinct domain codes; invalid internal availability is a separate programming/data-contract failure.
All assertions passed locally, including rejection of boolean quantities. The function only returns arithmetic; it does not reserve database inventory, authenticate a caller, handle concurrency, or guarantee a durable transaction. In a real application, the name or surrounding interface must make that distinction clear.
Core Concepts
Names carry observable promises
Prefer names that describe the operation a caller needs rather than the implementation accident used to perform it. Include units when ambiguity is plausible: timeout_seconds and timeout_ticks describe different interfaces. A name containing reserve should lead reviewers to ask whether it merely computes or actually commits a reservation.
Boundaries contain responsibility
A boundary is useful when it gives one component a coherent job and a stable interface. Splitting every line into a helper creates navigation without necessarily improving the design. Look for different invariants, trust levels, and reasons to change.
Errors are an interface
For HTTP APIs, RFC 9457 defines a problem-details representation. It also cautions against exposing implementation internals through errors. The representation does not choose your domain taxonomy or make error content safe automatically. Problem details and security
Compare the caller decisions before and after
This table is a design proposal, not a universal status-code mapping. Whether a client may retry depends on operation semantics, idempotency, and certainty about side effects. A friendly error message cannot make an unsafe retry safe.
Before refactoring, write down the decisions each caller currently makes. Then identify the distinctions the new contract must preserve. If a catch-all wrapper turns every exception into insufficient_stock, it has made the code look orderly while destroying useful information.
Choose stable codes for machine behavior and separate messages for explanation. A client should not parse English prose to decide whether to retry. Messages can evolve for clarity or localization without changing the meaning of a documented code.
Refactor one responsibility boundary at a time
Begin at the entry point. Parse the transport representation into validated types, apply the domain rule, perform the authoritative state change, and map the result to the caller’s interface. These may be separate components or simply clear sections, depending on the application’s size.
Keep the invariant near the authoritative update. A pure availability check followed by an unrelated write can race with another request. The example intentionally demonstrates naming and error meaning only; production reservation needs an appropriate transactional or conditional-write design.
Translate errors at a boundary that has enough context to improve their meaning. A storage adapter can preserve an operational cause for internal diagnosis while exposing a stable service-level failure. It should not guess that every database error means the user requested too many units.
Preserve causal information in approved internal logs or tracing without copying secrets and stack traces into public responses. Use a correlation reference when it helps support connect the external failure with internal evidence. Review what that reference and its lookup reveal to each audience.
Evolve the contract without surprising callers
List consumers before changing return values, exception types, or codes. A small internal helper may permit an immediate coordinated change; a public SDK or API can require compatibility handling and a documented transition.
Test invalid input, expected business rejection, operational failure, and uncertain completion independently. Confirm that each caller chooses the intended response. A test that checks only the error’s spelling misses the behavior the contract exists to support.
Make side effects visible in names and documentation. A function that calculates a price has a different promise from one that charges an account. If one operation performs both, its interface should make that consequence clear and explain partial failure.
Stop splitting when the next abstraction adds indirection without hiding a coherent policy. The goal is a reviewer who can follow the operation and its failure paths, not a directory full of impressively small functions.
Best Practices
Use domain terms consistently
Agree on what reservation, allocation, rejection, and completion mean in the application. Keep those meanings across code, API, and operator guidance.
Test recovery behavior
Exercise the caller’s next step for each meaningful error class. Include uncertain outcomes rather than assuming failure means nothing happened.
Review public detail
Expose enough information to repair or explain the request while keeping sensitive internals in the appropriate diagnostic system.
Common Mistakes
Bad: Renaming process fixes its design.
Correct: Clarify the operation, side effects, invariants, and outcomes before choosing the final name.
Bad: Every exception should become one friendly business error.
Correct: Preserve distinctions that change caller behavior and internal diagnosis.
Bad: More helpers always mean cleaner code.
Correct: Extract coherent responsibilities when the boundary reduces reasoning or isolates a changing policy.
FAQ
Should expected errors use exceptions or result values?
Either can work. Choose a consistent language-appropriate contract that makes important outcomes explicit and difficult to ignore.
Should domain code know HTTP statuses?
Usually keep transport mapping at the interface boundary when the domain also serves non-HTTP callers. The useful separation depends on the application.
What makes a name too long?
A name is too long when it obscures the operation or repeats context. If it lists many unrelated actions, reconsider the responsibility boundary.
Related Topics
- Clean Code — Readable implementation and maintainable responsibilities.
- Api Design — Public interface behavior and compatibility.
- Error Handling — Failure propagation and recovery design.