Software Architecture Is Mostly About Boundaries
TL;DR
If your architecture diagram looks beautiful but your team still spends half its time debugging data that crossed three layers and a queue, the problem is probably not your framework. It is your boundaries. Good architecture is mostly about deciding what is allowed to know what, who owns what, and where the ugly stuff is forced to stop. The rest is decoration.
The Lie We Tell Ourselves
Architects love grand nouns. Platforms. Ecosystems. Capability maps. Service meshes. It all sounds important, like a committee could fund it.
But the daily work of architecture is far less glamorous. It is the boring question of whether the payment service can reach into the user profile table “just this once.” It is whether the frontend is allowed to invent business rules because the backend was “busy.” It is whether six teams are calling the same thing by three different names and one of them is thing_v2_final_really_final.
Most system failures are boundary failures:
- data crosses a boundary without validation
- ownership crosses a boundary without agreement
- latency crosses a boundary without warning
- changes cross a boundary without versioning
That is the whole game. Everything else is support equipment.
Start With Ownership
Every meaningful boundary answers one question: who owns this?
If a service owns customer balances, then other services should not casually mutate those balances because the database was convenient and the password was already in the .env file. If a module owns pricing rules, do not let three other modules quietly reimplement “just the simple part” of the rule. That is how you end up with a pricing engine that behaves like a group project.
Ownership needs to be visible in code, not in a slide deck.
Good signs:
- one module has write access
- other modules talk to it through a narrow API
- invariants live near the data they protect
- breaking the rule feels hard, which is usually a good sign
Bad signs:
- everyone can write everywhere
- the same logic appears in five folders
- “temporary” helpers have become a religion
- the phrase “we should probably clean that up later” has a fossil record
Make Invalid States Annoying
The best architectures do not merely describe correct behavior. They make wrong behavior inconvenient.
That means:
- use types that exclude nonsense
- validate input at the edge
- keep internal representations small and boring
- separate commands from queries when the distinction matters
For example, if OrderStatus = "pending" | "paid" | "shipped" | "cancelled", do not also allow status = "maybe_paid" because one import script was in a hurry.
Likewise, if a function needs a fully validated object, make that obvious:
type CreateInvoiceInput = {
customerId: string;
amountCents: number;
currency: "USD" | "EUR";
};
function createInvoice(input: CreateInvoiceInput) {
// By the time we get here, the nonsense should already be gone.
}
The point is not to achieve purity. The point is to move confusion to the boundary, where it can be rejected with prejudice.
Boundaries Need Translation
Every boundary needs a translator. Not a person in a blazer, just code that converts one world into another.
Examples:
- HTTP request -> application command
- database row -> domain object
- queue message -> internal job
- third-party API payload -> local model
Translation is where architecture gets real. This is where you stop pretending the outside world shares your assumptions. It does not. The outside world is messy, late, duplicated, misspelled, and sometimes on a very strong coffee break.
So translate aggressively:
- rename fields to match domain language
- normalize weird external enums
- keep vendor-specific shapes at the edge
- fail fast when upstream data violates your contract
If a third-party API sends is_active: "true" as a string, absorb that shame at the edge and do not let it spread inward like a rumor.
The Boundary Budget
Every boundary has a cost:
- one more abstraction
- one more test
- one more place to translate data
- one more place to version changes
That is why “clean architecture” can go from elegant to self-parody if you build six layers to move a boolean from one function to another.
Use boundaries where the cost buys real safety:
- between teams
- between deployables
- between trust levels
- between stable core logic and chaotic external inputs
Do not create boundaries just to look principled. The code does not care about your principles. It cares about whether it can ship.
A Useful Heuristic
When deciding where to draw a line, ask:
- What can change independently?
- What must remain consistent?
- What needs to be tested in isolation?
- What failure should be contained?
If the answer to all four is “everything,” you do not have an architecture question. You have a design problem and possibly a calendar problem.
Closing Thought
Software architecture is not the art of making diagrams that survive screenshots. It is the discipline of building systems where the damage from one bad assumption cannot spread everywhere before lunch.
Draw fewer boundaries, but make the ones you keep enforceable.
Continue reading
Next article
Codexity Part 1: Architecture of an Answer Engine
Related Content
Modular Monoliths Win More Fights Than Microservices
Why the modular monolith is often the most honest architecture choice: less ceremony, fewer distributed systems surprises, and more time spent shipping actual product instead of negotiating with Kubernetes.
Hexagonal Architecture with FastAPI: Database, Valkey Cache, Messaging
Code-heavy walkthrough of a document management platform built with Hexagonal Architecture in Python. Includes FastAPI adapters, SQLAlchemy persistence, Valkey caching, and message publishing.
REST API Design: Beyond the Dogma
A pragmatic look at REST API design for developers who've already made mistakes and want to stop making them.