Module 7: What is Architecture?
Ask ten engineers "what is software architecture?" and you'll get twelve answers. Some will say "the high-level structure." Others will say "the important decisions." A few will say "whatever is hard to change later." All of these are partially right — and dangerously incomplete.
The IEEE 1471 standard defines architecture as: "The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution." Notice what's NOT in that definition: no mention of programming languages, frameworks, databases, or cloud providers. Architecture is about relationships, constraints, principles, and evolution — not technology selection.
The Real Definition: Boundaries, Interactions, Constraints, Evolution
Architecture defines four things:
- Boundaries — Where does one component end and another begin? What is internal (can change freely) vs. external (requires coordination to change)?
- Interactions — How do components communicate? Synchronous? Asynchronous? Event-driven? What contracts exist between them?
- Constraints — What are the hard limits? Regulatory compliance, latency budgets, team size, deployment frequency, cost ceilings.
- Evolution paths — How will this system change over time? What dimensions of change are easy? Which are intentionally expensive?
flowchart TD
subgraph NOT["❌ Not Architecture"]
N1["React vs Vue"]
N2["PostgreSQL vs MySQL"]
N3["AWS vs GCP"]
N4["REST vs GraphQL"]
end
subgraph IS["✅ Architecture"]
A1["Service boundaries"]
A2["Data ownership rules"]
A3["Communication patterns"]
A4["Failure isolation"]
A5["Evolution constraints"]
end
NOT --> |"Implementation
decisions"| IMPL["Easy to change
Low cost to reverse"]
IS --> |"Structural
decisions"| STRUCT["Hard to change
High cost to reverse"]
style N1 fill:#fff5f5,stroke:#BF092F,color:#132440
style N2 fill:#fff5f5,stroke:#BF092F,color:#132440
style N3 fill:#fff5f5,stroke:#BF092F,color:#132440
style N4 fill:#fff5f5,stroke:#BF092F,color:#132440
style A1 fill:#e8f4f4,stroke:#3B9797,color:#132440
style A2 fill:#e8f4f4,stroke:#3B9797,color:#132440
style A3 fill:#e8f4f4,stroke:#3B9797,color:#132440
style A4 fill:#e8f4f4,stroke:#3B9797,color:#132440
style A5 fill:#e8f4f4,stroke:#3B9797,color:#132440
style IMPL fill:#f0f4f8,stroke:#16476A,color:#132440
style STRUCT fill:#f0f4f8,stroke:#16476A,color:#132440
Martin Fowler puts it bluntly: "Architecture is about the important stuff. Whatever that is." The "important stuff" is context-dependent — for a high-frequency trading system, the network topology between matching engines IS architecture. For a marketing landing page, it's not. The architect's first job is identifying which decisions matter for this specific system in this specific context.
Quality Attributes: Measuring Architecture
Architecture manifests through quality attributes (also called "-ilities"). These are the measurable properties that distinguish a well-architected system from a poorly-architected one. They are NOT functional requirements (what the system does) — they are cross-cutting concerns about how well the system does it.
| Quality Attribute | Definition | Concrete Measurement | Architectural Impact |
|---|---|---|---|
| Availability | System is operational when needed | 99.99% uptime = 52 min downtime/year | Redundancy, failover, health checks |
| Scalability | Handle increasing load without degradation | Linear cost scaling: 2× traffic = ≤2.2× cost | Stateless services, sharding, async processing |
| Performance | Response time under load | p99 latency < 200ms at 10K RPS | Caching layers, data locality, connection pooling |
| Security | Protection against unauthorized access | Zero critical CVEs, SOC 2 Type II certified | Zero-trust, encryption at rest/transit, least privilege |
| Maintainability | Ease of making changes | Mean time to implement feature: < 3 days | Modularity, clear interfaces, low coupling |
| Operability | Ease of running in production | MTTR < 15 min, automated recovery for 80% of incidents | Observability, circuit breakers, graceful degradation |
| Testability | Ease of verifying correctness | Full integration test suite runs in < 10 min | Dependency injection, contract testing, feature flags |
The critical insight: quality attributes conflict. You cannot maximize all of them simultaneously. Optimizing for performance often hurts maintainability (caching adds complexity). Optimizing for security often hurts usability (MFA adds friction). Architecture is the art of choosing which attributes to prioritize and which to sacrifice — and documenting those tradeoffs explicitly.
# quality-attributes-spec.yaml
# Explicit quality attribute targets for an e-commerce platform
# These drive ALL architectural decisions — not technology preferences
system: order-processing-platform
version: "2.1"
date: "2026-05-15"
stakeholders:
- product: "Sub-second checkout experience"
- engineering: "Ship features weekly without breaking production"
- security: "PCI DSS Level 1 compliance"
- finance: "Infrastructure cost < 8% of revenue"
quality_attributes:
availability:
target: "99.95%"
measurement: "Successful requests / total requests (5xx excluded)"
budget: "4.38 hours downtime per year"
priority: 1 # Highest priority — revenue-critical
performance:
latency_p50: "45ms"
latency_p95: "120ms"
latency_p99: "200ms"
throughput: "15,000 requests/second sustained"
measurement: "End-to-end from client request to response"
priority: 2
scalability:
horizontal: true
cost_ratio: "2x traffic = max 2.3x cost"
scaling_time: "New instances serving traffic within 90 seconds"
peak_handling: "5x normal load during flash sales"
priority: 3
security:
compliance: ["PCI-DSS-L1", "SOC2-Type-II", "GDPR"]
encryption: "AES-256 at rest, TLS 1.3 in transit"
auth: "OAuth 2.0 + PKCE, session timeout 30 min"
priority: 4
maintainability:
deploy_frequency: "Multiple times per day"
lead_time: "Commit to production < 1 hour"
change_failure_rate: "< 5%"
priority: 5
operability:
mttr: "< 15 minutes for P1 incidents"
auto_recovery: "80% of known failure modes self-heal"
observability: "Distributed tracing on 100% of requests"
priority: 6
tradeoffs_accepted:
- "We accept higher infrastructure cost (scalability) to guarantee sub-200ms p99"
- "We accept deployment complexity (security) to maintain PCI compliance"
- "We sacrifice some maintainability (performance) by allowing strategic caching"
- "We accept lower test coverage on edge cases to maintain deploy frequency"
Architecture Decision Records (ADRs)
Every architecture embodies hundreds of decisions. Most of them are undocumented — living only in the heads of engineers who may have already left the company. Six months later, a new engineer asks: "Why are we using event sourcing here instead of a simple CRUD database?" Nobody remembers. The code becomes a archaeological artifact — studied but never truly understood.
Architecture Decision Records (ADRs) solve this by treating architectural decisions as first-class artifacts. Each ADR documents: what was decided, why, what alternatives were considered, and what tradeoffs were accepted. They're lightweight (1-2 pages), versioned in the repository alongside code, and accumulated over time to form the system's "decision log."
stateDiagram-v2
[*] --> Proposed: Engineer identifies
architectural decision
Proposed --> Accepted: Team reviews,
approves tradeoffs
Proposed --> Rejected: Alternative chosen,
document why
Accepted --> Deprecated: Context changed,
decision no longer optimal
Accepted --> Superseded: New ADR replaces
with better approach
Deprecated --> Superseded: Migration
completed
Rejected --> [*]
Superseded --> [*]
{
"adr": {
"id": "ADR-0042",
"title": "Use modular monolith over microservices for order domain",
"status": "Accepted",
"date": "2026-05-15",
"deciders": ["Sarah Chen (Staff Engineer)", "Marcus Webb (VP Engineering)"],
"context": {
"problem": "Order processing requires tight consistency guarantees across inventory, payment, and fulfillment. Our team of 8 engineers cannot sustain the operational overhead of distributed transactions across microservices.",
"constraints": [
"Team size: 8 engineers (4 senior, 4 mid-level)",
"Consistency requirement: Orders must never oversell inventory",
"Deploy target: Multiple deploys per day",
"Budget: Cannot afford dedicated SRE team yet"
]
},
"decision": "Implement the order domain as a modular monolith with clear module boundaries enforced by ArchUnit tests. Modules communicate via in-process method calls with defined interfaces. Data isolation enforced at schema level (separate schemas, shared database).",
"alternatives_considered": [
{
"option": "Microservices with saga pattern",
"pros": ["Independent scaling", "Technology diversity", "Fault isolation"],
"cons": ["Distributed transaction complexity", "Operational overhead for 8-person team", "Network latency on hot path"],
"rejected_because": "Operational cost exceeds our team capacity. Saga pattern adds weeks of development for compensating transactions."
},
{
"option": "Traditional layered monolith",
"pros": ["Simple", "Fast development initially"],
"cons": ["No module boundaries", "Becomes big ball of mud", "Cannot extract later"],
"rejected_because": "No enforcement of boundaries means coupling will grow unchecked. We've seen this fail at scale."
}
],
"consequences": {
"positive": [
"Single deployment unit — simple CI/CD",
"In-process calls — zero network latency between modules",
"Strong consistency via database transactions",
"Team can reason about entire system in one codebase"
],
"negative": [
"Must scale entire application together (cannot scale order processing independently)",
"Must enforce module boundaries through discipline + automated tests",
"Technology lock-in: all modules share same language/runtime"
],
"risks": [
"If team grows to 20+, monolith may become coordination bottleneck",
"Module boundaries may erode without vigilant ArchUnit enforcement"
]
},
"review_date": "2027-01-15",
"supersedes": null,
"related_adrs": ["ADR-0038", "ADR-0039"]
}
}
ATAM: Architecture Tradeoff Analysis Method
The Software Engineering Institute (SEI) developed ATAM as a structured process for evaluating architecture against quality attribute requirements. It's not about finding "the best architecture" — it's about surfacing tradeoffs and risks before they become production incidents.
ATAM works by identifying sensitivity points (where a small change in the architecture dramatically affects a quality attribute) and tradeoff points (where improving one quality attribute degrades another). For example:
- Sensitivity point: The database connection pool size. Too small → requests queue and latency spikes. Too large → database runs out of connections and crashes.
- Tradeoff point: Adding a cache layer improves performance (latency drops from 200ms to 5ms) but hurts consistency (stale data visible for TTL duration) and increases operational complexity (cache invalidation bugs).
flowchart TD
ARCH["Architecture
Decision"] --> QA1["Performance ⬆️"]
ARCH --> QA2["Maintainability ⬇️"]
ARCH --> QA3["Security ⬆️"]
ARCH --> QA4["Usability ⬇️"]
QA1 --> T1["Caching adds latency benefit
but stale data risk"]
QA2 --> T2["Cache invalidation logic
increases code complexity"]
QA3 --> T3["Zero-trust adds protection
but authentication overhead"]
QA4 --> T4["MFA reduces breach risk
but adds user friction"]
T1 --> TRADEOFF["Every architectural decision
is a TRADEOFF — document it"]
T2 --> TRADEOFF
T3 --> TRADEOFF
T4 --> TRADEOFF
style ARCH fill:#e8f4f4,stroke:#3B9797,stroke-width:2px,color:#132440
style QA1 fill:#e8f4f4,stroke:#3B9797,color:#132440
style QA2 fill:#fff5f5,stroke:#BF092F,color:#132440
style QA3 fill:#e8f4f4,stroke:#3B9797,color:#132440
style QA4 fill:#fff5f5,stroke:#BF092F,color:#132440
style TRADEOFF fill:#f0f4f8,stroke:#16476A,stroke-width:2px,color:#132440
Evolution Over Perfection
The most dangerous misconception in software architecture: that you must get it right upfront. That architecture is a one-time act of genius that produces a blueprint to be followed forever. This is waterfall thinking applied to structural decisions — and it fails for the same reasons waterfall fails for features.
Good architecture is designed to evolve. It acknowledges that today's constraints will change, today's team will grow, today's traffic patterns will shift. The architect's job is not to predict the future — it's to make the system cheap to change in the directions that matter and resilient to surprises in directions that don't.
Principles for evolutionary architecture:
- Defer decisions until the last responsible moment — don't choose a database on day 1 when you haven't seen real query patterns yet. Use repository interfaces and decide later.
- Make boundaries explicit — even in a monolith, define module interfaces so that extraction to a service is a mechanical refactor, not an archaeological dig.
- Use fitness functions — automated checks that verify architectural properties are maintained as code evolves (dependency direction, cycle-free modules, response time budgets).
- Embrace "good enough" — architecture that ships and can be improved beats perfect architecture that never ships.
Module 8: Monolithic Architectures
In the era of microservices hype, admitting you run a monolith feels like confessing to using a flip phone. But here's what the industry's most successful companies know: monolithic architecture is not a failure state — it's often the optimal choice. Shopify runs a $5B+ commerce platform on a modular monolith. Basecamp serves millions on a single Rails application. Stack Overflow handled 1.3 billion page views per month with a monolithic .NET application.
A monolith is simply a system where all functionality deploys as a single unit. That's it. It says nothing about internal organization, code quality, or scalability. A well-structured monolith with clear module boundaries can be a thing of architectural beauty. A poorly-structured microservice system can be a distributed nightmare of tangled dependencies.
The Layered Monolith
The most traditional monolithic pattern: horizontal layers where each layer has a specific responsibility and can only call the layer directly below it. This was the dominant architecture for enterprise applications from the 1990s through the 2010s.
flowchart TD
CLIENT["Client
(Browser / Mobile)"] --> PRES
subgraph MONOLITH["Single Deployment Unit"]
PRES["Presentation Layer
Controllers, Views, API Endpoints"]
BIZ["Business Logic Layer
Domain Services, Rules, Workflows"]
DATA["Data Access Layer
Repositories, ORM, Queries"]
DB["Database
Single shared schema"]
PRES --> BIZ
BIZ --> DATA
DATA --> DB
end
style CLIENT fill:#f0f4f8,stroke:#16476A,color:#132440
style PRES fill:#e8f4f4,stroke:#3B9797,color:#132440
style BIZ fill:#dff0f0,stroke:#3B9797,color:#132440
style DATA fill:#d4ebeb,stroke:#3B9797,color:#132440
style DB fill:#c9e5e5,stroke:#3B9797,color:#132440
style MONOLITH fill:#fafffe,stroke:#3B9797,stroke-width:2px,color:#132440
Layer responsibilities:
- Presentation: HTTP handling, request/response serialization, input validation, authentication middleware. Knows about the shape of external requests but nothing about business rules.
- Business Logic: Domain rules, workflows, calculations, orchestration. The "brain" of the system. Should be testable without HTTP or database dependencies.
- Data Access: Translates between domain objects and database records. Repository pattern, ORM mappings, query optimization. Hides storage implementation details from business logic.
- Database: Single shared schema. All tables accessible from any data access repository. This is both the strength (transactional consistency) and weakness (coupling) of the layered monolith.
The problem with layers: They enforce horizontal separation (by technical concern) but don't enforce vertical separation (by business domain). A "User" entity might be referenced by the Order layer, the Shipping layer, the Billing layer, and the Notification layer — creating implicit coupling across the entire codebase. Changing the User schema requires coordinating changes in every layer that touches it.
The Modular Monolith: Best of Both Worlds
A modular monolith takes the deployment simplicity of a monolith and combines it with the structural discipline of service-oriented architecture. Instead of horizontal layers, it's organized into vertical modules aligned to business domains — each module owns its data, business logic, and API surface, but all modules deploy together as a single application.
flowchart TD
CLIENT["Client"] --> GW["API Gateway / Router"]
subgraph MONOLITH["Single Deployment Unit"]
subgraph MOD_ORDER["Orders Module"]
O_API["Orders API"]
O_BIZ["Order Logic"]
O_DB["Orders Schema"]
O_API --> O_BIZ --> O_DB
end
subgraph MOD_INV["Inventory Module"]
I_API["Inventory API"]
I_BIZ["Inventory Logic"]
I_DB["Inventory Schema"]
I_API --> I_BIZ --> I_DB
end
subgraph MOD_PAY["Payments Module"]
P_API["Payments API"]
P_BIZ["Payment Logic"]
P_DB["Payments Schema"]
P_API --> P_BIZ --> P_DB
end
GW --> O_API
GW --> I_API
GW --> P_API
O_BIZ -.->|"Interface call
(not direct DB access)"| I_API
O_BIZ -.->|"Interface call"| P_API
end
style CLIENT fill:#f0f4f8,stroke:#16476A,color:#132440
style GW fill:#f0f4f8,stroke:#16476A,color:#132440
style MONOLITH fill:#fafffe,stroke:#3B9797,stroke-width:2px,color:#132440
style MOD_ORDER fill:#e8f4f4,stroke:#3B9797,color:#132440
style MOD_INV fill:#e8f4f4,stroke:#3B9797,color:#132440
style MOD_PAY fill:#e8f4f4,stroke:#3B9797,color:#132440
Key rules of the modular monolith:
- Modules own their data — the Inventory module NEVER directly queries the Orders schema. It calls the Orders module's public interface instead.
- Communication through defined interfaces — modules expose public APIs (in-process method calls). Internal implementation is hidden behind these interfaces.
- No circular dependencies — if Module A depends on Module B, Module B CANNOT depend on Module A. Use events for reverse communication.
- Boundaries enforced by tooling — not just "agreed upon" in a meeting. ArchUnit tests, package-private visibility, or separate compilation units enforce the boundaries.
When to Choose a Monolith
The industry has overcorrected toward microservices. Here's when a monolith (layered or modular) is the architecturally correct choice — not a compromise, but the optimal design for your constraints:
| Condition | Why Monolith Wins | Microservice Cost |
|---|---|---|
| < 10 developers | Team can hold entire system in their heads. No coordination overhead. | Distributed systems complexity consumes 40%+ of small team's capacity |
| Early startup (pre-PMF) | Requirements change daily. Refactoring one codebase is fast. | Refactoring service boundaries requires rewriting inter-service contracts |
| Strong consistency required | ACID transactions across domains. No eventual consistency bugs. | Distributed transactions (saga/2PC) add weeks of complexity per flow |
| Performance-critical hot path | In-process calls: nanoseconds. No serialization/network overhead. | Each service hop adds 1-5ms latency, serialization cost, failure modes |
| Simple domain | CRUD apps don't benefit from distribution. Complexity is not justified. | Microservices add operational cost that exceeds the domain's complexity |
| No DevOps/SRE capacity | One server, one deploy pipeline, one log stream. | Kubernetes, service mesh, distributed tracing, multi-pipeline CI/CD |
Strengths and Weaknesses — Honest Assessment
- Simplicity of deployment: One artifact, one deploy command, one rollback
- Easier debugging: Stack traces show the full call path. No distributed tracing needed.
- Lower operational overhead: No service mesh, no distributed tracing infrastructure, no multi-service orchestration
- Transactional consistency: Database ACID guarantees span all operations naturally
- Refactoring safety: IDE-assisted refactoring across the entire codebase in one PR
- Onboarding speed: New engineers learn one codebase, one deployment, one set of tools
- Scaling limits: Must scale the entire application — can't scale just the hot component
- Organizational coupling: Large teams stepping on each other's changes; merge conflicts grow with team size
- Technology lock-in: All modules share the same language, runtime, and framework
- Blast radius: A memory leak in one module crashes the entire application
- Deploy coupling: Changing one module requires redeploying everything — and testing everything
Case Studies
Shopify's Modular Monolith at Scale
Shopify processes $444 billion in GMV (2023) through a single Ruby on Rails monolith — one of the largest monolithic applications in production anywhere. But it's not a "big ball of mud." Starting in 2019, Shopify undertook a deliberate transformation into a modular monolith using a system they call "componentization."
How it works:
- The monolith is divided into ~320 components (modules) with explicit public interfaces
- Components declare their dependencies explicitly — undeclared dependencies are CI failures
- A custom tool ("Packwerk") statically analyzes component boundaries and flags violations
- Each component owns its database tables — cross-component joins are forbidden
- Teams own components, not layers — a team responsible for "Checkout" owns the full vertical slice
Why not microservices? Shopify's engineering leadership concluded that for their domain (commerce), the tight consistency requirements between inventory, orders, and payments made distributed transactions a worse tradeoff than the scaling limitations of a monolith. Instead, they invested in making the monolith modular — getting the boundary discipline of microservices without the operational cost.
Result: 3,000+ engineers contribute to the same codebase with clear ownership boundaries, high deploy frequency, and no distributed systems debugging overhead.
Basecamp's Majestic Monolith Philosophy
In 2016, DHH (creator of Ruby on Rails, CTO of Basecamp) published "The Majestic Monolith" — arguing that for most companies, a well-crafted monolith is architecturally superior to microservices. Not "good enough." Superior.
Basecamp's numbers:
- Millions of users served by a single Rails application
- Team of ~20 programmers maintaining the entire platform
- Deployment: single "git push" to production multiple times per day
- No dedicated SRE team — developers handle their own operations
- Infrastructure cost: significantly lower than comparable microservice architectures
DHH's argument: Microservices are an organizational scaling strategy, not a technical one. If your team is small enough to coordinate effectively, the overhead of distributed systems (network failures, eventual consistency, service discovery, contract versioning) is pure cost with no benefit. The "majestic monolith" is a deliberate architectural choice — not a failure to adopt microservices.
Key principle: "If you can fit your entire team in one room and everyone can understand the full system, you don't need microservices. You need good module boundaries and testing discipline."
Exercises
Exercise 1: Write Your First ADR
Pick an architectural decision in your current system that is NOT documented. Maybe it's "why we use PostgreSQL instead of MongoDB" or "why the authentication service is separate from the main application." Write an ADR for it using this template:
{
"adr": {
"id": "ADR-XXXX",
"title": "Your decision title here",
"status": "Accepted",
"date": "2026-05-15",
"deciders": ["Your name", "Other stakeholders"],
"context": {
"problem": "What problem prompted this decision?",
"constraints": [
"What limits did you have?",
"Team size, budget, timeline, compliance?"
]
},
"decision": "What did you decide and HOW will it be implemented?",
"alternatives_considered": [
{
"option": "Alternative 1",
"pros": ["Advantage 1", "Advantage 2"],
"cons": ["Disadvantage 1", "Disadvantage 2"],
"rejected_because": "Why this alternative lost"
}
],
"consequences": {
"positive": ["Good outcome 1", "Good outcome 2"],
"negative": ["Accepted downside 1", "Accepted downside 2"],
"risks": ["What could go wrong?"]
},
"review_date": "When should this decision be re-evaluated?"
}
}
Challenge: Share your ADR with a teammate who wasn't part of the original decision. Can they understand the "why" without asking follow-up questions? If not, your context section needs more detail.
Exercise 2: Architecture Fitness Functions
Fitness functions are automated tests that verify your architectural properties remain intact as code evolves. They run in CI and fail the build if architectural invariants are violated.
#!/bin/bash
# architecture-fitness-functions.sh
# Run these in CI to enforce architectural boundaries
echo "=== Architecture Fitness Functions ==="
echo ""
# Fitness Function 1: No circular dependencies between modules
echo "[1/4] Checking for circular dependencies..."
CYCLES=$(find src/modules -name "*.ts" -exec grep -l "import.*from.*modules/" {} \; | \
while read file; do
MODULE=$(echo "$file" | sed 's|src/modules/\([^/]*\)/.*|\1|')
IMPORTS=$(grep "from.*modules/" "$file" | sed "s|.*modules/\([^/']*\).*|\1|")
for imp in $IMPORTS; do
if grep -r "from.*modules/$MODULE" "src/modules/$imp/" > /dev/null 2>&1; then
echo "CYCLE: $MODULE <-> $imp"
fi
done
done | sort -u)
if [ -n "$CYCLES" ]; then
echo "❌ FAIL: Circular dependencies detected!"
echo "$CYCLES"
exit 1
else
echo "✅ PASS: No circular dependencies"
fi
# Fitness Function 2: Module boundaries respected
echo ""
echo "[2/4] Checking module boundary violations..."
VIOLATIONS=$(grep -r "from.*modules/.*/internal" src/modules/ --include="*.ts" | \
grep -v "from.*modules/$(basename $(dirname $file))/internal" 2>/dev/null)
if [ -n "$VIOLATIONS" ]; then
echo "❌ FAIL: Modules accessing internal APIs of other modules!"
echo "$VIOLATIONS"
exit 1
else
echo "✅ PASS: Module boundaries respected"
fi
# Fitness Function 3: Response time budget
echo ""
echo "[3/4] Checking API response time budget..."
P99=$(curl -s http://localhost:9090/api/v1/query \
--data-urlencode 'query=histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))' | \
jq -r '.data.result[0].value[1]' 2>/dev/null)
if [ -n "$P99" ] && (( $(echo "$P99 > 0.200" | bc -l) )); then
echo "❌ FAIL: p99 latency ${P99}s exceeds 200ms budget!"
exit 1
else
echo "✅ PASS: p99 latency within 200ms budget (current: ${P99:-N/A}s)"
fi
# Fitness Function 4: Deployment size limit
echo ""
echo "[4/4] Checking deployment artifact size..."
ARTIFACT_SIZE=$(du -sm dist/ 2>/dev/null | cut -f1)
MAX_SIZE=150 # MB
if [ -n "$ARTIFACT_SIZE" ] && [ "$ARTIFACT_SIZE" -gt "$MAX_SIZE" ]; then
echo "❌ FAIL: Deployment artifact ${ARTIFACT_SIZE}MB exceeds ${MAX_SIZE}MB limit!"
exit 1
else
echo "✅ PASS: Deployment artifact within size budget (current: ${ARTIFACT_SIZE:-N/A}MB)"
fi
echo ""
echo "=== All fitness functions passed ==="
// archunit-module-boundaries.js
// Automated enforcement of modular monolith boundaries
// Run as part of CI pipeline — fails build on boundary violation
const fs = require('fs');
const path = require('path');
const MODULES_DIR = path.join(__dirname, '../src/modules');
const RESULTS = { passed: 0, failed: 0, violations: [] };
// Rule 1: Modules can only import from other modules' public API (index.ts)
function checkPublicApiImports() {
const modules = fs.readdirSync(MODULES_DIR);
modules.forEach(moduleName => {
const moduleDir = path.join(MODULES_DIR, moduleName);
const files = getAllFiles(moduleDir, '.ts');
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const imports = content.match(/from\s+['"]\.\.\/\.\.\/([^'"]+)['"]/g) || [];
imports.forEach(imp => {
const importedModule = imp.match(/\.\.\/\.\.\/([^/]+)/);
if (importedModule && importedModule[1] !== moduleName) {
// Cross-module import — must be from index (public API)
if (!imp.includes(`../../${importedModule[1]}'`) &&
!imp.includes(`../../${importedModule[1]}"`)) {
RESULTS.violations.push({
rule: 'public-api-only',
file: file.replace(MODULES_DIR, ''),
message: `Imports internal path of module "${importedModule[1]}"`
});
RESULTS.failed++;
}
}
});
});
});
RESULTS.passed++;
console.log(`[Public API Rule] ${RESULTS.violations.length === 0 ? '✅' : '❌'}`);
}
// Rule 2: No module can have more than 5 direct dependencies
function checkDependencyFanOut() {
const modules = fs.readdirSync(MODULES_DIR);
modules.forEach(moduleName => {
const moduleDir = path.join(MODULES_DIR, moduleName);
const files = getAllFiles(moduleDir, '.ts');
const dependencies = new Set();
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const imports = content.match(/from\s+['"]\.\.\/\.\.\/([^/'"]+)/g) || [];
imports.forEach(imp => {
const dep = imp.match(/\.\.\/\.\.\/([^/'"]+)/);
if (dep && dep[1] !== moduleName) dependencies.add(dep[1]);
});
});
if (dependencies.size > 5) {
RESULTS.violations.push({
rule: 'max-dependencies',
file: moduleName,
message: `Module "${moduleName}" has ${dependencies.size} dependencies (max: 5)`
});
RESULTS.failed++;
}
});
console.log(`[Dependency Fan-Out Rule] ${RESULTS.failed === 0 ? '✅' : '❌'}`);
}
function getAllFiles(dir, ext) {
let results = [];
const items = fs.readdirSync(dir, { withFileTypes: true });
items.forEach(item => {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) results = results.concat(getAllFiles(fullPath, ext));
else if (item.name.endsWith(ext)) results.push(fullPath);
});
return results;
}
// Execute
console.log('=== Module Boundary Enforcement ===\n');
checkPublicApiImports();
checkDependencyFanOut();
console.log(`\nResults: ${RESULTS.passed} rules passed, ${RESULTS.failed} violations`);
if (RESULTS.violations.length > 0) {
console.log('\nViolations:');
RESULTS.violations.forEach(v => console.log(` ❌ [${v.rule}] ${v.file}: ${v.message}`));
process.exit(1);
}
Conclusion & Next Steps
The two modules in this part establish the foundation for everything that follows in this series:
- Module 7 taught you that architecture is about boundaries, interactions, constraints, and evolution — not technology choices. Quality attributes are how you measure architecture, and they always involve tradeoffs. ADRs make those tradeoffs visible and durable.
- Module 8 taught you that monolithic architecture is a legitimate, often optimal, architectural choice — not a starting point to escape from. The modular monolith in particular gives you service-level boundaries without distributed systems complexity.
The key mental model: start with the simplest architecture that satisfies your quality attribute requirements, and evolve toward complexity only when concrete evidence demands it. Premature distribution is at least as dangerous as premature optimization — and far more expensive to reverse.
When do you need something beyond a monolith? When your team outgrows it (Conway's Law — Part 4). When your quality attributes require independent scaling. When fault isolation becomes critical. When different modules need different technology stacks. That's when you move to microservices — not because it's "modern," but because your constraints demand it.
Next in the Series
In Part 6: Microservices Architecture, we'll explore when and how to decompose systems into independently deployable services — service boundaries, inter-service communication, data ownership, and the real operational cost that nobody warns you about until it's too late.