| name | architecture |
| description | Use when reviewing architectural concerns: module boundaries, dependency direction, layer discipline, bounded-context isolation, ADR coverage, premature abstraction, and structural fitness for change |
Architecture
Overview
Architecture is the set of structural decisions that shape what a system can become. It is the part hardest to change after the fact, so the part most worth getting right deliberately. Architecture is not "the diagram"; it is the constraint regime that determines whether the next change is local or global.
The scope of architecture is requirements plus architectural characteristics: the functional behavior the system delivers and the critical capabilities (reliability, maintainability, scalability, security, auditability, performance, modularity, …) it must preserve while delivering it. Both shape structure.
Core principle: The strategy of architecture is to leave as many options open for as long as possible. Dependencies point toward stability; modules hide secrets; decisions leave receipts; structural seams match team and agent boundaries; architectural characteristics are named and mechanically verified. Each named principle below is a way of preserving the option to change.
Core Principles
1. Information hiding (modules hide a secret)
A module is a unit of decomposition organized around something it hides from its callers: a data structure, an algorithm choice, a third-party API. The interface is the contract; the secret is whatever can change behind it without breaking callers.
A module that hides nothing (a "junk drawer" utils/, common/, core/) is not a module; it is a coupling point.
2. Dependency direction (point toward stability)
Dependencies point from volatile details toward stable abstractions. High-level modules (use cases, domain logic) do not import low-level concrete types (database drivers, HTTP clients, filesystem helpers); they depend on abstractions they own.
When the arrow points the other way, every detail change ripples upward through the stable core.
3. Layer discipline (one axis of change per layer)
Validation, pricing, authorization, and workflow rules are domain concerns; they do not live in HTTP handlers, ORM models, or view templates. Layering exists so each module changes for one reason. When business logic lives in the transport layer, it cannot be reused, tested without spinning the layer up, or moved without rewriting it.
Multiple valid layering schools exist: clean architecture, hexagonal (ports and adapters), onion, vertical-slice. The principle is: pick one and apply it consistently. The red flag is mixing schools without a stated rule, not "did not pick hexagonal."
4. No cycles in the module graph
Module A imports B and B imports A (directly or transitively) means A and B are one module pretending to be two. Neither can be reasoned about, tested, or replaced in isolation, and the build graph stops being a graph.
The Acyclic Dependencies Principle is non-negotiable in any layering scheme.
5. Rule of three for abstraction extraction
The cheapest abstraction is the one extracted from three concrete cases. Speculative abstractions ("for future flexibility") locked in from one or two cases lock in the wrong shape; future changes route around the abstraction rather than through it, accumulating accidental complexity.
This principle scopes to abstraction extraction, not to constants or configuration. A constant defined once is fine; an interface defined for one caller is not.
6. Bounded-context isolation (scope: systems with multiple contexts)
This principle scopes to systems large enough to have multiple bounded contexts. A single-context POC inherits a weaker form: keep the option to split later by not coupling internal models to transport.
When the scope applies: each context owns its data and its model; cross-context coupling (importing internal types, querying another context's tables, sharing mutable in-process objects) collapses two contexts into one with two names. Integration between contexts goes through explicit messaging or APIs.
7. ADR coverage for non-obvious decisions
Architectural decisions evaporate the moment they are made unless captured. ADRs record context, decision, and consequences at the moment of choice, so the next contributor (or agent) can tell deliberate from accidental.
ADR coverage is a precondition for the system to remain editable across team changes; without it, every load-bearing decision becomes a candidate for relitigation.
Code that implements a load-bearing decision back-references the ADR in a top-of-file comment (or equivalent locality marker), using the exact ADR filename slug (e.g., 0042-postgres-to-cockroach) so the reference is greppable. During a refactor, grep -r 0042-postgres-to-cockroach returns every file implementing that decision, which is the only mechanically-reliable way to find them. Free-text back-references ("see the database ADR") cannot be grepped, drift silently, and lose the receipt. ADRs without back-references rot quietly; back-references without ADRs are unverifiable claims.
8. Sociotechnical alignment (Conway's Law)
"Any organization that designs a system will produce a design whose structure mirrors the organization's communication structure" (Conway, 1968). The corollary is the design imperative: pick the team and collaboration shape you want, and make the architectural seams match it. For small, parallel, cross-functional teams (and increasingly, parallel agents), seams must be drawn so each team or agent can work in isolation without serializing on shared state.
Two tactical realizations recur:
- Vertical slice architecture. Slices run end-to-end through transport, application, and domain for a single capability. High cohesion within a slice, low coupling between slices. This permits parallel work because each slice changes independently; the cost is that cross-cutting consistency must be designed in deliberately rather than assumed.
- Command Query Responsibility Segregation (CQRS). Commands (writes) and queries (reads) have different consistency, latency, and scaling needs; treating them as one model fixes the architecture to the more constraining of the two. Splitting them lets each scale on its own clock.
Both are sketched here; deep-dives live in dedicated companion docs: event sourcing, event modeling, and CQRS in event-sourcing/README.md; vertical-slice patterns in vertical-slice/README.md.
When seams do not match the team boundaries, every feature crosses a coordination boundary; teams (and agents) serialize on each other and the speed of change collapses regardless of how clean the dependency graph looks.
9. Evolutionary architecture: architectural characteristics verified by fitness functions
This principle, named and developed by Neal Ford, Rebecca Parsons, Patrick Kua, and Pramod Sadalage in Building Evolutionary Architectures, treats architectural decisions the way the rest of the codebase treats domain logic: as claims that need automated checks to stay true under change.
Architectural characteristics are the named non-functional capabilities the system must preserve: reliability, scalability, security, auditability, performance, modularity, evolvability, accessibility, and others appropriate to the domain. They are not universal; they are picked per system and trade off against each other. Naming them is the first step; an unnamed characteristic cannot be designed for, and can only erode silently.
Fitness functions are automated checks that validate architectural decisions. They are to architectural characteristics what unit tests are to domain integrity. Each named characteristic gets one or more mechanical checks that verify it is preserved as the code evolves: architecture tests for module-graph and import-direction rules; code metrics for coupling, instability, abstractness, distance from main sequence, cyclomatic and cognitive complexity; security scans for the security characteristic; performance budgets and load tests for performance; chaos experiments for resilience; SLO checks for reliability. The system-wide fitness function is the union of the individual ones, used to evaluate trade-offs when a proposed change shifts one characteristic at the expense of another.
Architects measure outcomes (the objective fitness measure), not implementations. Without fitness functions, every architectural rule decays to folklore between code reviews; with them, rules are checked on every commit and the architecture evolves under guard rather than under hope. In an agentic codebase, where humans review a smaller fraction of changes, this is closer to a precondition than a nice-to-have.
For the deep-dive (metric vocabulary, fitness function categories, system-wide composition, pipeline integration, anti-patterns, maturity progression), see fitness-functions/README.md.
Cross-reference: the types skill carries the same fitness-function pattern applied to type-system invariants the checker cannot enforce alone (silent Any leakage, untyped dict returns, escape-hatch sprawl). The dependencies skill carries the pattern applied to supply-chain commitments (zero-dep entry points, transitive depth caps, package-count budgets); see dependencies/dependency-minimization/README.md for the dependency-specific instances. The security skill carries the pattern applied to committed security rules (no eval outside sandbox, no string-concat SQL, no plaintext secrets in logs). The environments skill carries the pattern applied to committed parity rules (runtime-version drift, image-content audit, dependency-tree drift, seed-data minimums).
Red Flags - STOP
- Architectural decision (database swap, new service boundary, framework replacement) shipped without an ADR or rationale comment in the diff
- Circular dependencies between modules (direct or transitive)
- High-level module importing concrete database driver, HTTP client, or filesystem helper directly (Dependency Inversion violation)
- Business logic (validation, pricing, authorization, workflow) inside HTTP handlers, ORM models, or view templates
- A
common/, utils/, core/ package imported almost everywhere and growing unboundedly (god module)
- New abstraction (base class, interface, generic helper) introduced to support one or two callers (rule of three violation)
- One bounded context importing another's internal types or querying its tables directly
- Layering style mixed across the codebase with no stated convention
- Load-bearing decision implemented in code with no back-reference to an ADR (or no ADR at all)
- Architectural seams crossing the same lines as team or agent boundaries on every feature; every change requires coordination across the seam (Conway's-Law misalignment)
- Vertical slices that share mutable state across slice boundaries (cohesion-coupling inverted)
- Reads and writes share a single model whose consistency, latency, and scaling needs are pulling against each other (missing CQRS where it would relieve real pressure)
- The system has no named architectural characteristics; the "non-functional requirements" are implicit
- Architectural rules exist only in code review checklists, never in CI; no fitness functions for the named characteristics
- Code metrics (coupling, instability, abstractness, cyclomatic / cognitive complexity, import direction) trend monotonically worse and nothing in the pipeline notices
Rationalization Prevention
| Excuse | Reality |
|---|
| "We'll write the ADR later" | Later doesn't come; the rationale lives in a Slack thread that expires |
| "It's just a small cycle" | A cycle means the modules are one module; reasoning collapses regardless of size |
| "Importing the driver directly is simpler" | It is, until the driver changes and every use case has to change with it |
| "Business logic in the controller is fine for now" | Now becomes the next five years; the rule duplicates the next time it is needed |
| "We need this abstraction for future flexibility" | Future flexibility from one case is a guess; from three cases it is a pattern |
| "Bounded contexts are over-engineering" | They are, until two teams ship conflicting changes to the same model |
| "utils.py is just a place for shared helpers" | It is the de facto coupling point; every change to it is a change to everything |
"The ADR is in docs/adrs/; the code doesn't need to mention it" | Receipts unreachable from code rot; the back-reference is what keeps the rationale alive at the point of edit |
| "We don't need to think about team shape, the architecture is clean" | A clean dependency graph that crosses team boundaries on every feature still serializes the teams; Conway wins |
| "Vertical slices duplicate code" | Some duplication across slices is the price of independent change; premature DRY across slices is the bug |
| "Reads and writes can share one model" | Until they can't; CQRS is cheap before scale forces it and expensive after |
| "We have non-functional requirements, we just haven't written them down" | Unnamed characteristics aren't designed for; they degrade silently |
| "The architecture rules are in the wiki / code review checklist" | Rules unenforced rot; mechanize them as fitness functions or accept they will erode |
Key Patterns
✅ Use case imports a Repository interface it owns; adapter implements it
❌ Use case imports psycopg2.connect directly
✅ HTTP handler: parse request → call use case → format response
❌ HTTP handler: parse, validate business rules, query DB, compute price, format response
✅ docs/adr/0042-switch-to-cockroach.md (context, decision, consequences)
❌ Database swap shipped with diff only; rationale in a stale Slack thread
✅ Module graph: domain ← application ← interface; no back-edges
❌ domain imports application for a "shared helper"; cycle established
✅ Abstraction extracted from three concrete cases that already exist
❌ Abstraction introduced for "future flexibility" with one caller
✅ Context A talks to Context B via published API or message
❌ Context A queries Context B's tables directly
✅ payments/processor.py: top-of-file comment names ADR by exact slug `0042-idempotent-charges` (greppable across the repo)
❌ payments/processor.py: comment says "see the idempotency ADR"; not greppable, not reliably findable during refactor
✅ Slice payments/refunds/* owns its routes, use case, repository; no slice imports another slice's internals
❌ refunds/use_case.py imports orders/internal/calculator.py; slices coupled through internals
✅ Write side (commands) goes through aggregate; read side (queries) hits a denormalized projection
❌ Single ORM model serves both list pages and high-throughput command path; one constrains the other
✅ Named characteristics (reliability, security, performance) each have at least one fitness function in CI
❌ "Non-functional requirements" listed in the wiki; nothing in CI verifies them
✅ dependency-cruiser / import-linter / ArchUnit rule fails the build on a back-edge
❌ Architecture rules live only in the code review checklist
Why This Matters
Architecture is the part of a system that resists change most. Get it wrong early and every subsequent change pays a tax; get it right and the system absorbs new requirements without rewrites. The principles above are not preferences; they are the constraints that keep change local and keep the named architectural characteristics intact under evolution.
The outcomes architecture serves are, in Kleppmann's framing, reliability, maintainability, and scalability, plus whichever additional characteristics the system has named (security, auditability, performance, evolvability, …). Each of these is achievable only if the structural decisions support it; each is verifiable only if the rules are mechanized.
Without architectural discipline:
- Every change becomes global; the team slows down as the codebase grows.
- Decisions get relitigated because no one remembers why they were made.
- Bounded contexts collapse into a single distributed monolith.
- Premature abstractions become permanent shapes the codebase routes around.
- Conway's Law wins by default, and the team or agent shape the architecture forces is the one you get, not the one you want.
- Architectural rules erode silently between code reviews because nothing checks them.
With architectural discipline:
- Changes stay local because modules hide secrets behind contracts.
- Decisions are receipts; the rationale survives team changes, with code back-references keeping it reachable from the point of edit.
- Contexts evolve on their own clock without coupling to neighbors.
- Abstractions reflect three real cases, not one imagined future.
- Seams match team and agent boundaries; parallel work is structurally possible.
- Architectural characteristics are named, and fitness functions catch erosion on every commit.
Growth examples
Soft sketch; not a checklist. Where appropriate is shaped by the target's maturity.
- POC / prototype: one module, one process, one data store. No ADRs yet; decisions are still fluid. Architectural characteristics are not yet named; that is fine. Awareness that the first cycle, the first god module, and the first leaked detail will lock shape if not noticed.
- Growing internal tool: explicit module boundaries appear; the first ADRs document the choices that already feel load-bearing, and the implementing code back-references them. Layering convention is named (one of the schools). Public-vs-internal distinction enforced by language or convention. The two or three architectural characteristics that actually matter for this tool are named.
- Shared library: dependency direction enforced; the library does not import application-level concerns. Public API is documented; internal modules hide their secrets. ADRs cover every load-bearing decision and are back-referenced from the code that implements them. First fitness functions appear: at minimum, a module-graph check and a public-API surface check.
- Production service: layering enforced in review or by automated checks. Module graph is acyclic and validated mechanically. ADRs are part of the workflow; non-obvious changes ship with one. Bounded contexts (if multiple) are isolated and integrate via published contracts. Architectural seams reflect team boundaries deliberately. Each named characteristic has at least one fitness function in CI (architecture tests, security scans, performance budgets, SLO checks as appropriate).
- Safety-critical: architectural decisions reviewed by a second pair of eyes; ADRs are part of the release artifact. Module boundaries enforced mechanically. Cross-context integration uses explicit, versioned, monitored contracts. Premature abstraction caught in review before it lands. The system-wide fitness function is composed of individual ones across all named characteristics and is treated as a release gate; trade-offs between characteristics are made with the fitness data on the table, not from intuition.
References & rationales
- David Parnas, "On the Criteria to Be Used in Decomposing Systems into Modules" (CACM, 1972). Backs principle 1 (information hiding). The foundational argument that interfaces are designed around what to hide.
- John Ousterhout, A Philosophy of Software Design (2018). Backs principle 1 from the depth direction. Deep modules over shallow ones; complexity as the dominant force; restraint on shallow utility packages.
- Robert C. Martin, Clean Architecture (2017). Backs principles 2 (Dependency Rule, dependencies point inward) and 4 (Acyclic Dependencies Principle). Also backs the framing of the core as durable and the details (UI, frameworks, databases) as the volatile periphery.
- Martin Fowler, Patterns of Enterprise Application Architecture (2002). Backs principle 3 (layer discipline, presentation/domain/data separation).
- Alistair Cockburn, "Hexagonal Architecture" (2005). Backs principles 2 and 3 from the ports-and-adapters direction. The original ports-and-adapters formulation; core is ignorant of drivers and driven side.
- Vaughn Vernon, Implementing Domain-Driven Design (2013). Backs principles 2, 3, and 6 from the operational-DDD direction. Working treatment of hexagonal architecture, aggregates, value objects, and bounded-context integration patterns.
- Frederick Brooks, The Mythical Man-Month (1975) and "No Silver Bullet" (1986). Backs principle 5 (rule of three). Essential vs accidental complexity; speculative abstraction is accidental complexity by construction.
- Eric Evans, Domain-Driven Design (2003). Backs principle 6 (bounded contexts). Strategic-design vocabulary for context boundaries; ubiquitous language as the cross-team communication primitive.
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns (2003). Corroborates principle 6 from the integration direction. Messaging vocabulary for cross-context integration.
- Michael Nygard, "Documenting Architecture Decisions" (2011). Backs principle 7 (ADR coverage). The canonical ADR template and the rationale-capture practice. The back-reference-from-code practice extends Nygard with a locality discipline.
- Melvin Conway, "How Do Committees Invent?" (Datamation, 1968). Backs principle 8. The original statement of the law: system structure mirrors organizational communication structure.
- Matthew Skelton and Manuel Pais, Team Topologies (2019). Backs principle 8 from the design-imperative direction. Stream-aligned teams and the inverse-Conway maneuver: choose the team shape, then design seams to match.
Suggested technologies (as of 2026-04-28)
These go stale fast; the date is the "as-of." Verify currency before adopting. The principles above outlast specific tools; if a tool here is no longer maintained, the patterns transfer to its replacement.
- Architecture testing (mechanical enforcement): ArchUnit (Java/Kotlin),
dependency-cruiser (JS/TS), import-linter (Python), archtest (Go).
- Code-metric fitness functions: language-native complexity linters in CI (cyclomatic and cognitive complexity rules); SonarQube as a multi-language aggregator when one is needed.
- Security fitness functions:
gitleaks for secrets; Google's osv-scanner for dependency vulnerabilities (SCA); Semgrep and GitHub CodeQL for SAST; OWASP ZAP for DAST.
- Performance / reliability fitness functions: k6 for load testing; SLO checks via Prometheus and Grafana (or the cloud-native equivalent).
- ADR tooling:
adr-tools CLI, log4brains, MADR template, plain markdown in docs/adr/.
- Hexagonal / ports-and-adapters scaffolding: language-native; rarely needs a framework. Spring (Java) and NestJS (TS) provide DI primitives that align well; Go and Rust prefer explicit constructor injection.
- Module-graph visualization:
dependency-cruiser --output-type dot, pydeps, cargo-modules, go-callvis.
- Bounded-context messaging: Kafka, NATS, RabbitMQ, cloud-native (SQS/SNS, Pub/Sub, EventBridge); the choice is downstream of the integration pattern.
- Event sourcing / CQRS infrastructure: Syntropic137 EventSourcing Platform as the primary recommendation in this ecosystem. Alternatives in broader use include EventStoreDB and Marten (Postgres-backed); choice is downstream of the consistency and replay requirements.
- Diagrams as code: Mermaid (rendered by GitHub natively), Structurizr (C4 model), PlantUML, D2.
Continual improvement
This skill is maintained at:
https://github.com/syntropic137/software-leverage-points/blob/main/skills/architecture/SKILL.md
To improve it, edit the file directly and follow the chassis discipline in maintaining-software-leverage-points: regenerate catalogs, run just qa, then commit.