Architecture Review
Identify primitives. Draw black boxes. Wrap dependencies.
Core principle: "It's faster to write five lines of code today than to write one line today and then have to edit it in the future." - Eskil Steenberg
When to Use
Always use for:
- Planning new modules or subsystems
- Major refactors affecting 3+ files
- Adding external dependencies
- Code that "works but feels tangled"
- Pre-review of architectural decisions
Skip for:
- Bug fixes within existing architecture
- Adding features to well-defined modules
- Configuration changes
The Four-Phase Review
Phase 1: Primitive Identification
Find the fundamental data types that flow through the system.
Questions to answer:
- What is the basic unit of information?
- What operations are performed on this data?
- Can this primitive handle future requirements without change?
Examples by domain:
Web service:
- Request (method + path + headers + body)
- Response (status + headers + body)
- User (id + credentials + permissions)
Data pipeline:
- Record (schema + values + metadata)
- Transform (input_type → output_type)
- Batch (records + timestamp + source)
Checklist:
- List all data types that cross module boundaries
- Each primitive has clear, minimal definition
- Primitives are stable (won't need frequent changes)
- Operations on primitives are well-defined
Phase 2: Black Box Boundaries
Every module should be replaceable using only its interface.
The test: Can you describe this module to a new developer using ONLY:
- What goes in (inputs)
- What comes out (outputs)
- What guarantees it provides
Checklist:
- Each module has explicit input/output types
- Internal implementation details are hidden
- Interface is documented without reference to internals
- Module can be rewritten without touching callers
Red flags:
# BAD: Leaky abstraction
class DataProcessor:
def process(self, data):
self._internal_buffer.append(data) # Caller knows about buffer
return self._cached_results[-1] # Caller knows about caching
# GOOD: Clean interface
class DataProcessor:
def process(self, data: InputType) -> OutputType:
"""Processes data and returns result."""
... # Implementation hidden
Phase 3: Dependency Audit
Never depend directly on what you don't control.
External dependencies to wrap:
- Third-party libraries (pandas, requests, boto3)
- Platform APIs (file system, network)
- Data sources (databases, APIs, message queues)
- Configuration systems
Checklist:
- External libraries accessed through wrapper layer
- No direct imports of external APIs in business logic
- Wrappers have stable interfaces even if underlying changes
- Dependencies can be swapped without touching core code
Example wrapper pattern:
# platform/storage.py - Wrapper layer
from typing import Protocol
class StorageProtocol(Protocol):
"""Our interface - stable even if S3/GCS/local changes."""
def read(self, key: str) -> bytes: ...
def write(self, key: str, data: bytes) -> None: ...
# Business logic imports from wrapper, not boto3 directly
from platform.storage import StorageProtocol
Phase 4: Replaceability Test
For each component, answer:
| Question | Good Answer | Bad Answer |
|---|---|---|
| Can one developer understand this? | "Yes, it does X" | "Well, you need to understand Y and Z first..." |
| Can this be rewritten from scratch? | "Yes, given the interface" | "No, too many implicit dependencies" |
| What breaks if this fails? | "Just this feature" | "Half the system" |
| Can we add 10x features here? | "Yes, same interface" | "We'd need to restructure" |
Checklist:
- Single developer can own each module
- Failure in one module doesn't cascade
- Clear boundaries enable parallel development
- Future features fit existing structure
Architecture Patterns
Good Architecture
┌─────────────────────────────────────────────────────────┐
│ External Layer (wrapped) │
│ └─ APIs, databases, file systems, third-party libs │
├─────────────────────────────────────────────────────────┤
│ Service Layer (primitives: request → response) │
│ └─ Each service is independent black box │
├─────────────────────────────────────────────────────────┤
│ Domain Layer (primitives: input → output) │
│ └─ Business logic, pure functions where possible │
├─────────────────────────────────────────────────────────┤
│ Infrastructure Layer (wrapped) │
│ └─ Logging, metrics, configuration │
└─────────────────────────────────────────────────────────┘
Bad Architecture Signs
- Lower layers import from upper layers
- Business logic depends on specific infrastructure
- Business logic scattered across layers
- "Utility" modules used everywhere
- Circular imports
Quick Reference
| Phase | Key Question | Success Criteria |
|---|---|---|
| 1. Primitives | What data flows through? | Minimal, stable types defined |
| 2. Black Boxes | Can I describe the interface without internals? | Yes, input/output only |
| 3. Dependencies | Do I control this? | If not, wrapped |
| 4. Replaceability | Can one person rewrite this? | Yes, using only interface |
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Wrapper adds overhead" | Wrapper prevents rewrite-everything-later |
| "Only I work on this" | You in 6 months won't remember |
| "Dependencies are stable" | Until they're not |
| "Too small to modularize" | Small modules grow; boundaries don't |
| "Flexibility for later" | Over-abstraction is as bad as under-abstraction |
Integration with Plan Agent
When using plan agent for architecture design:
- Run architecture review FIRST
- Identify primitives and boundaries
- Let plan agent design within those constraints
- Review plan output against this checklist
Output Format
After review, document:
## Architecture Review: [Component/System Name]
### Primitives
- [List with definitions]
### Module Boundaries
| Module | Input | Output | Responsibility |
|--------|-------|--------|----------------|
### Dependency Map
- External: [wrapped/unwrapped status]
- Internal: [dependency direction]
### Risks
- [Identified architectural risks]
### Recommendations
- [Concrete changes needed]