Testability, Tradeoffs & Common Pitfalls
Why This Enables Fast Tests
Because business logic only depends on abstractions, Use Cases can be unit-tested with in-memory fakes for the repository interface — no real database needed, tests run in milliseconds.
class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();
async findById(id: string) { return this.users.get(id) ?? null; }
async save(user: User) { this.users.set(user.id, user); }
}
test('transfers funds between two accounts', async () => {
const repo = new InMemoryUserRepository();
// no real DB — fast, isolated, deterministic
});DTOs & Presenters
A DTO moves data across a boundary without exposing an Entity's internal structure, keeping the domain model independent of what the outer layer needs. A Presenter formats data (dates, currency) for a specific UI, so the same Use Case output can serve a web page, a CLI, or a JSON API without changing the Use Case.
Leaky Abstractions
// BAD — leaks SQL into the abstraction, defeats the purpose
interface UserRepository {
query(sqlFragment: string): Promise<User[]>;
}
// GOOD — expressed in domain terms, genuinely swappable
interface UserRepository {
findActiveUsersInRegion(region: string): Promise<User[]>;
}When It's Overkill
Full layering adds real upfront complexity — interfaces with one implementation, DTOs mirroring an Entity 1:1, mapping code between near-identical layers. This cost is justified by expected complexity and longevity: apply the principles proportionally rather than as a rigid checklist for every project regardless of size.
Databases & Frameworks Are "Details"
A defining, sometimes counter-intuitive stance: the choice of Postgres vs. MongoDB, or REST vs. CLI, shouldn't dictate the shape of core business logic — those choices should be swappable implementation details, not the starting point of the design.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free