Making Architecture Decisions
Architecture decisions are consequential and hard to reverse. Never decide from memory or pattern-matching alone — inspect first, decide second, document third. The simplest architecture that satisfies verified requirements beats the most "scalable" one.
Activate when:
- Starting structural design for a new project or new major subsystem
- A requested change affects module boundaries, data flow, service topology, or dependency direction
- Choosing between competing approaches (e.g., monolith vs. services, sync vs. async, SQL vs. NoSQL, REST vs. events)
- Planning a migration, large refactor, or framework/platform change
- Existing architecture docs look stale or contradict the code
- Someone asks "how should this be structured?" or "what's the right pattern here?"
Do NOT activate for:
- Implementing a feature within established boundaries
- Isolated bug fixes, typo fixes, styling, copy changes
- Writing tests for existing behavior
- Performance tuning of a single function/query (unless it reveals a structural flaw)
When in doubt: if the change can be described as "add/fix this one thing" without touching boundaries or contracts, it's implementation, not architecture.
This skill decides structure and contracts, not implementation detail.
| Specialist | Handoff |
|---|---|
| Frontend | This skill defines component/module boundaries and API contracts the UI consumes; frontend specialist owns UI implementation, state management detail, styling. |
| Backend | This skill defines service boundaries, data flow, and interfaces; backend specialist owns endpoint implementation, business logic, ORM/query code. |
| Debugging | If debugging reveals a structural root cause (e.g., leaky abstraction, wrong boundary), hand off here to decide the fix's architecture; routine bug diagnosis stays with debugging. |
| Security | Flag security-relevant trade-offs (trust boundaries, auth placement) but defer deep threat modeling and vulnerability analysis to the security specialist. |
| Performance | Use quality attribute scenarios to set performance requirements/targets; defer profiling and micro-optimization to the performance specialist. |
| Technical research | Delegate open technology comparisons needing external/current data (e.g., "which vendor/library is best right now") to research, then consume findings as evidence. |
| Testing | Define testability boundaries and what needs contract/integration tests; defer test case design and execution to the testing specialist. |
Progress:
- [ ] 1. Inspect reality (existing code, docs, deps, interfaces)
- [ ] 2. Establish requirements, quality attributes, constraints, acceptance criteria
- [ ] 3. Generate realistic alternatives
- [ ] 4. Compare trade-offs explicitly
- [ ] 5. Choose simplest sufficient option
- [ ] 6. Define boundaries, contracts, data flow
- [ ] 7. Document decision (ADR / C4 update) if significant
- [ ] 8. Hand off actionable spec to implementation specialists
Step 1: Inspect Reality (never trust memory)
For existing projects, before proposing anything:
- Read actual directory structure, not an assumed one
- Open key source files for the area under discussion — don't infer behavior from filenames or prior conversation
- Check actual dependency manifests (package.json, pom.xml, requirements.txt, go.mod, etc.) for real versions and what's actually used vs. declared
- Check config files (env, CI/CD, IaC) for deployment topology truth
- Read existing architecture docs/diagrams/ADRs — then verify against code; flag contradictions explicitly rather than trusting the doc
- Identify existing module boundaries and current dependency direction (what depends on what)
For new projects, inspect instead:
- Any existing scaffolding, chosen stack constraints, team conventions
- Stated business/product constraints (deadline, team size, compliance)
Never propose structural changes based on what you "recall" the codebase looking like — re-read it.
Step 2: Establish Requirements Before Deciding
Do not pick an architecture until you can state:
- Functional requirements — what must the system do (specific, not "handle users")
- Quality attributes — expressed as testable scenarios, e.g.:
"Under 500 concurrent users, 95th percentile checkout latency < 300ms" "System must survive single AZ failure with < 1 min downtime" Not vague goals like "fast" or "scalable."
- Constraints — team size/skills, budget, timeline, existing stack, compliance/regulatory, hosting limits
- Acceptance criteria — how we'll know the decision succeeded
If these are missing or vague, ask or state explicit assumptions — do not invent plausible-sounding requirements and proceed as if they were given. Label anything not explicitly confirmed as ASSUMPTION.
If a requirement is unclear but not consequential (low cost to change later), proceed with a stated assumption rather than blocking. If it's consequential (hard/costly to reverse), surface the uncertainty and seek confirmation before committing.
Step 3: Generate Realistic Alternatives
Produce 2–3 genuinely viable options, not a strawman + the preferred answer. Include "do nothing structural / smallest change" as a baseline option when applicable. Ground options in what's evidenced (Step 1), not in generic best-practice lists.
Step 4: Compare Trade-offs Explicitly
For each alternative, address what's relevant to the decision (not every axis every time):
- Maintainability — cognitive load, coupling, how changes ripple
- Performance — against the quality attribute scenarios from Step 2
- Reliability — failure modes, blast radius, recovery
- Security — trust boundaries, attack surface changes
- Operational complexity — deployment, monitoring, on-call burden, new moving parts
- Cost — infra, licensing, engineering time
- Migration risk — if changing existing systems: data migration, downtime, rollback path, blast radius if wrong
Use a short trade-off table when 2+ alternatives are being seriously compared. Skip formal analysis for low-stakes, easily reversible decisions.
Step 5: Choose the Simplest Sufficient Option
Default to the option that satisfies Step 2's requirements with the least structural complexity. Reject additions justified only by:
- "It's a best practice" (without a tied requirement)
- "We might need it later" (speculative future-proofing)
- "It's more scalable" (when no scale requirement demands it)
- Familiarity/popularity of a framework without fit justification
Explicitly name what you are not adding and why (e.g., "No message queue: no requirement for async decoupling or burst absorption exists today").
Step 6: Define Boundaries and Contracts
When the decision involves structure, specify concretely:
- Component/module responsibilities (one sentence each, non-overlapping)
- Dependency direction (who may depend on whom; what's forbidden)
- Data flow (what moves where, sync vs async, ownership of data)
- Interfaces/API contracts (inputs, outputs, error semantics) — enough that an implementation specialist doesn't have to invent them
This is the deliverable implementation specialists consume. Be concrete: name modules, not placeholders like "service layer handles stuff."
Step 7: Document When Justified
Match documentation effort to decision significance:
- Trivial/reversible → no formal doc, just explain reasoning inline
- Moderate → a short ADR (see format below)
- Significant (new service boundary, data model change, platform shift) → ADR + update/create a C4 diagram (Context/Container level usually sufficient; Component level only if genuinely useful) + update architecture map
ADR format:
Markdown# ADR-NNN: <decision title>
Proposed | Accepted | Superseded by ADR-XXX
What requirement/problem forced this decision. Key constraints. Evidence inspected.
What we chose, stated plainly.
- Option A — rejected because...
- Option B — rejected because...
What becomes easier, harder, or risky as a result. Follow-up work created.
Store ADRs in `docs/adr/` (or project convention if one exists — check first). Update existing architecture docs rather than leaving contradictions for future readers; note what changed and why.
### Step 8: Produce Actionable Output
Final output to implementation specialists must include:
- The decision and its rationale (brief)
- Concrete boundaries/contracts from Step 6
- Explicit non-goals ("do not add X")
- Open risks/uncertainties still unresolved, flagged as such
Do not leave gaps for implementers to fill with invented requirements — if something is genuinely undecided, say so rather than letting it be silently assumed.
- C4 Model — for communicating system structure at Context/Container/Component levels. Use Context+Container for most decisions; Component level only for complex internals worth mapping.
- ADRs — for recording why, not just what. Use for any decision that's hard to reverse or non-obvious in hindsight.
- Quality attribute scenarios — turn vague "-ilities" into testable statements (stimulus, environment, response, measure).
- Dependency analysis — trace actual import/call graphs before claiming a boundary is clean or dirty; don't assume from folder names.
- Trade-off/decision matrices — for 2+ seriously competing alternatives with multiple relevant quality attributes.
Pick what the decision needs. A one-line config change doesn't need a C4 diagram; a new service boundary probably needs an ADR at minimum.
Example 1 — Existing project, proposed change Input: "Should we split the monolith into microservices to scale better?" Output:
- Inspect: read actual codebase, find it's a Django monolith, 12k LOC, one Postgres DB, deployed as a single container, current traffic ~50 req/s peak.
- Requirements check: ask/confirm what "scale better" means — turns out it's deploy frequency pain (one team blocked by another's release), not request volume.
- Alternatives: (a) do nothing structural, improve CI/CD and feature flags; (b) modularize within the monolith (clear internal boundaries, enforce via lint rules); (c) extract 1 high-churn module into a service.
- Trade-offs: microservices add operational complexity (new deploy pipelines, service discovery, distributed debugging) not justified by current scale; real pain is release coupling.
- Decision: Option (b) — modularize internally first; revisit service extraction only if a specific module's team-ownership/deploy-cadence conflict remains after that. ADR written; no C4 container diagram change yet (still one deployable unit), but component-level module map added.
- Explicit non-goal: "Not introducing Kubernetes/service mesh — no evidence of need."
Example 2 — New project Input: "We're building a new internal tool for 20 users to track equipment maintenance." Output:
- Requirements: CRUD app, 20 users, low traffic, team of 2 devs, 6