Loading
Loading
Design Workflow
Testability, concurrency, error handling and boundaries — the considerations that only appear once the design has to run.
By the end of this chapter
When a class is hard to test, the usual cause is not the test framework. It is that the class constructs its own collaborators, reads the clock directly, or reaches global state — so there is no way to place it in a known situation.
| Hard to test because | The design fix |
|---|---|
| It calls `Instant.now()` | Inject a Clock |
| It constructs its own gateway | Inject the interface |
| It reads a static singleton | Pass the dependency in |
| It does five things | It is five classes |
| Its result is only a side effect | Return something meaningful |
The instinct when concurrency appears is to add locks where the errors happen. That produces a system where every method is synchronised, throughput collapses, and deadlock becomes possible in places nobody can enumerate.
Concept
Not 'where do I need a lock', but 'which single type owns this mutable state'. Once one type owns it, thread safety is that type's problem and nowhere else's.
How a method fails is part of its interface, not an implementation detail. A method that returns null on failure, throws on a different failure, and logs-and-continues on a third has three contracts and callers will get at least one wrong.
Decide, per operation: is failure expected and part of normal flow, or exceptional? Expected failures — a space not being available, a code already taken — are better as return values the caller must handle. Genuinely exceptional ones — a corrupt configuration — are better as exceptions. Mixing the two arbitrarily is what makes error handling unreadable.
The last thing worth protecting is the edge of your domain. A domain type carrying @Column annotations, or shaped by what a JSON API happens to return, is coupled to that technology for as long as it exists. The database schema will change for database reasons and the API will change for API reasons, and both will reach into your domain logic.
The alternative is a translation step at each boundary: a persistence type, a transport type, and a domain type that knows about neither. The cost is mapping code, which is real and tedious. The benefit is that your rules survive a schema migration. For a small system the coupling is affordable; for one expected to live for years, the mapping usually pays for itself.
Sign in to keep a note here. It is free, and nobody else can see it.
Sign inWhat this chapter was for: