Make the Boundary Obvious
A module that cannot explain what it owns will eventually own everything. Notes on contracts, ownership, and the small interfaces that keep a codebase legible.
On this page
The hardest bugs are rarely inside a function. They are at the seams: two modules that both believe they own the same rule, a cache that hides a write, a client that assumes the server already validated something. The code inside each piece can be clean and the system can still be wrong.
Boundaries are the real architecture. Not the folders, not the diagram on the wall. The boundary is the answer to three questions: what does this module own, what does it need, and what is it allowed to change?
A boundary is a contract, not a wall
A wall stops communication. A boundary defines it. The best modules I have worked with have a small public surface and a clear statement of responsibility. The rest is private — not because it is secret, but because it is allowed to change.
In a TypeScript codebase, the contract is usually a type and a function:
export { createInvoice } from "./use-case";
export { getInvoice, listInvoices } from "./queries";
export type { Invoice, InvoiceStatus } from "./domain";Three exports. Everything else in the folder is an implementation detail. If a consumer needs something that is not exported, that is a design conversation, not an invitation to deep-import.
In a Spring Boot codebase, the contract is a service interface and a DTO. The implementation can use JPA, JDBC, or a remote call. The controller should not know which. The moment a controller imports a repository, the boundary has disappeared and the database is now part of the HTTP layer.
The interface is the documentation
A good interface tells you what is true after the call. A bad interface tells you how the call is implemented.
// Good: the caller knows what it gets.
issueInvoice(invoiceId: string): Promise<IssuedInvoice>
// Bad: the caller now knows about the database.
issueInvoice(invoiceId: string, tx: EntityManager): Promise<ResultSet>The second signature leaks a dependency. It also makes the call harder to test, harder to retry, and impossible to move behind a queue. The rule is not “never pass a transaction.” The rule is “do not make the caller responsible for your storage strategy.”
The same idea applies to front-end boundaries. A useCheckout() hook should expose state and actions, not the shape of the API response. If the component knows the response envelope, every API change becomes a UI change.
Boundary smells
I look for these in code review:
| Smell | What it usually means |
|---|---|
| A file imports from another feature’s internals | The public surface is incomplete, or the feature is two features |
A shared utils/ folder with 40 files | The boundary is “miscellaneous” |
| A type used by the database and the UI | Persistence shape is leaking into presentation |
| A service that calls three repositories and formats a response | The use case and the adapter are in the same function |
| A cache invalidation in a controller | The write path is not owned by the module that owns the data |
| A test that mocks five collaborators | The module has too many reasons to change |
None of these are automatically wrong. A small app can survive a shared utils folder for a long time. The problem is when the smell becomes the default and nobody can say where a change belongs.
Make the seam visible in the code
The best boundary is one a new engineer can find without asking. That means names, file placement, and the shape of imports all point at the same idea.
In a feature-first Next.js app, I use index.ts as the public surface and a lint rule to stop deep imports. In a monorepo, I use package boundaries and explicit exports. In a microservice system, I use an OpenAPI contract and a generated client. The mechanism changes; the intention does not: crossing the boundary should be a deliberate act.
There is a social side to this. A boundary that only exists in a diagram will be crossed the first time someone is in a hurry. A boundary that fails a build will be respected. That is not bureaucracy; it is making the easy path the correct path.
NOTE
A boundary that cannot be explained in one sentence is probably not a boundary. “This module owns invoice issuance and the events that follow it” is a boundary. “This module handles business logic” is a wish.
The test of a boundary
I use a simple thought experiment. If the implementation behind this boundary were replaced tomorrow — a different database, a different provider, a different UI — what would break?
If the answer is “the callers, because they know the implementation,” the boundary is decorative. If the answer is “the adapter, and the contract tests,” the boundary is real.
That is the whole game. The boundary is not there to make the diagram look architectural. It is there so that change has a location. The code inside can be messy for a week. The seam should stay obvious for years.
A short checklist
- Does this module have a public surface, and is it smaller than its implementation?
- Can I name what it owns in one sentence?
- Does its interface expose outcomes, not mechanisms?
- Are its dependencies pointed in one direction?
- Is there a test that would fail if the contract changed?
- Could a new engineer find the boundary without opening the folder?
If the answer to the last question is no, the boundary is not obvious yet. Make it obvious. The rest of the architecture follows.