What architecture is (and isn't)
"Architecture is the set of decisions that are expensive to change later." - Ralph Johnson
Architecture is not "which framework did we use" - that's an implementation detail. Architecture is the shape you feel when you navigate the codebase: how modules are split, in which direction dependencies flow, what code can call what.
Good architecture tells an engineer where to write the new feature without ambiguity. Bad architecture turns that question into a debate each time.
The cost of decisions
The real cost of an architectural decision shows up not when it's made, but as the number of downstream decisions grows. Three measures:
- Reversal time - how many weeks to undo?
- Spread surface - how many files / modules depend on this decision?
- Cognitive load - how many hours does a new engineer need to grasp it?
Before locking in a decision ask: if I realize this is wrong in three months, how painful is the reversal?
Intention vs. implementation
Architecture is the set of decisions you intended; the codebase is what actually got built. The gap between the two is architectural drift - and it grows quietly until a change that should take an hour takes a week.
The job isn't to draw the perfect diagram once. It's to keep intention and implementation close enough that the diagram still predicts how the code behaves.
Layered (n-tier)
The classic split: presentation → application → domain → data. Each layer may only call the one beneath it. It's the easiest model to teach and the easiest to violate - one call from the UI straight into the data layer and the layering is a fiction.
Use it when the team is small and the domain is shallow. Its weakness is that business logic tends to leak upward into controllers and downward into the database.
Hexagonal · Ports & Adapters
The domain sits in the centre and knows nothing about the outside world. Everything external - database, HTTP, message queue - talks to it through ports (interfaces) that adapters implement.
The payoff is testability and replaceability: you can swap Postgres for an in-memory fake in tests, or REST for gRPC, without touching a line of domain code.
Clean Architecture
A refinement of hexagonal: concentric rings where dependencies only ever point inward, toward the domain entities. Frameworks, UI and databases live on the outermost ring - details, not the core.
It buys you a domain that compiles and tests without any infrastructure. The cost is ceremony - more interfaces, more mapping. Worth it for long-lived systems, overkill for a CRUD admin panel.
DDD - Domain-Driven Design
DDD isn't a framework - it's a vocabulary discipline. "Aggregate", "bounded context", "anti-corruption layer" - a shared language to talk about the problem.
When DDD?
- The domain is complex, often confused with technical complexity.
- Multiple teams work on the same domain.
- Business and engineering speak different languages.
DDD isn't for every project. If it feels heavy for a CRUD-heavy admin panel, it probably is.
Big Ball of Mud
No discernible architecture: everything depends on everything, state is shared globally, and the only way to understand a change is to run it. Most systems arrive here not by decision but by a thousand deadlines.
You don't rewrite a ball of mud - you strangle it. Wrap a seam, move one capability behind a clean interface, repeat. AppArch's dependency map shows you where the seams already are.
Antipattern: God module
A "god module" is the one called from everywhere, containing everything. Names like Utils, Common, Shared are warning signs.
# a rule that detects it - id: GOD-001 max_fan_in: 25 message: "Module imported by >25 files - split or rename"
Fix: split into roughly three. Group one is real infra (logger, config). Group two is domain primitives (money, id). Group three is probably a separate module hiding in there.
Circular dependencies
Module A needs B, B needs A. Now neither can be understood, tested or deployed alone, and the build order becomes a coin flip. Cycles are the single most reliable predictor of a module nobody wants to touch.
Break the cycle by extracting the shared concept into a third module both depend on, or by inverting one direction with an interface. AppArch flags cycles the moment they appear in a merge request.
Leaky abstractions
An abstraction leaks when you have to know how it's implemented to use it correctly - a repository that throws SQL exceptions, a cache that forces callers to handle staleness. It promised to hide a detail and didn't.
Either seal the leak - translate implementation errors at the boundary - or drop the abstraction. A leaky one is worse than none: you pay for the indirection without getting the isolation.
Architecture review
A review isn't a gate that blocks merges; it's a conversation about whether a change moves the system toward or away from its intended shape. The best reviews ask 'what does this make hard later?' more than 'is this correct now?'
Keep it lightweight and continuous. A short review on every significant merge request beats a quarterly architecture board that everyone routes around.
Architecture Decision Records
An ADR captures one decision: the context, the options considered, the choice, and its consequences - a paragraph or two, versioned next to the code. Six months later it answers the only question that matters: why is it like this?
Write the ADR when the decision is made, not after. Supersede rather than delete - the trail of why you changed your mind is as valuable as the current answer.
Large refactoring
A big refactor that lands as one giant merge is a rewrite in disguise - and it gets reverted the first time it breaks. Real refactoring is a sequence of small, behaviour-preserving steps, each shippable on its own.
Add the new path beside the old, migrate callers incrementally, then delete the old path. Tests are the safety rope; without them you're not refactoring, you're gambling.