Theoretical Foundations
Welcome to the curriculum workspace. Here you will find long-form technical guidelines outlining core architectural blueprints and implementation mechanics.
Module 1: Foundational Design Paradigms & SOLID
PHASE 1 β MICRO-ARCHITECTURE: This module establishes the code-level design principles that govern every component boundary and interface contract in your system. Master these principles before progressing to architectural styles in Phase 2. Every distributed system you design in Modules 4β10 rests on whether the individual services inside it were built with these fundamentals.
Introduction: The Anatomy of Code Rot
Software that starts clean degrades over time. Engineers call this process code rot β the gradual deterioration of a codebase's internal quality while its external behavior remains (mostly) intact.
Code rot does not appear overnight. It accumulates through hundreds of small, individually reasonable decisions made under time pressure:
Week 1: "I'll just add this email-sending logic directly to OrderService for now."
Week 4: "I need to add a logging statement here to debug this billing issue."
Week 8: "The new PayPal integration was easier to bolt onto the existing class."
Week 16: "Nobody touches OrderService. It's too risky."
By the time a team recognizes the rot, the component has become a Big Ball of Mud β a system with no recognizable structure, where everything depends on everything else.
The Measurable Cost of Tight Coupling
Robert C. Martin's research on software quality quantified coupling using two metrics:
Fan-Out (Efferent Coupling β $C_e$): The number of classes or modules that a class depends on.
Fan-In (Afferent Coupling β $C_a$): The number of classes that depend on a given class.
From these, he derived the Instability Metric ($I$): $$I = \frac{C_e}{C_a + C_e}$$
Where $I = 0$ is maximally stable (many dependents, no outgoing dependencies) and $I = 1$ is maximally unstable (no dependents, many outgoing dependencies).
He also defined the Abstractness Metric ($A$): $$A = \frac{N_a}{N_c}$$ Where $N_a$ is the number of abstract classes/interfaces and $N_c$ is the total number of classes in the package.
The ideal design follows the Main Sequence β a line on the $(A, I)$ plane from $(A=0, I=1)$ (concrete, unstable) to $(A=1, I=0)$ (abstract, stable):
Abstractness (A)
|
1.0 | (Zone of Uselessness)
| *
| *
| *
0.5 | * <- Main Sequence
| *
| *
0.0 | (Zone of Pain) *
+---------------------> Instability (I)
0.0 0.5 1.0
The Zone of Pain ($A \approx 0, I \approx 0$) β concrete classes with many dependents β is dangerous. Changing anything breaks everything. The Zone of Uselessness ($A \approx 1, I \approx 1$) β highly abstract classes with no dependents β are dead code.
The Distance from the Main Sequence measures design quality: $$D = |A + I - 1|$$ A value near zero means the component is well-positioned. Values approaching 1 indicate structural problems.
Section 1: The Five SOLID Principles
SOLID is an acronym for five principles introduced by Robert C. Martin ("Uncle Bob") and popularized through his books Clean Code and Agile Software Development. These principles are not abstract theory β they are precise engineering rules that directly reduce $D$ (distance from the main sequence) across your codebase.
A. Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
The word "responsibility" is often misunderstood to mean "one function." SRP is about change axes β a class should be affected by changes originating from only one actor (user, stakeholder, or business process).
The Problem: Multi-Axis Mutation
// VIOLATION: OrderService has three change axes
class OrderService {
// Axis 1: Business Logic (changed by Product team)
calculateTotal(items: CartItem[]): number {
return items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
// Axis 2: Data Persistence (changed by Database team)
saveOrder(order: Order): void {
const sql = `INSERT INTO orders (id, total) VALUES (${order.id}, ${order.total})`;
db.execute(sql);
}
// Axis 3: Notification (changed by Marketing team)
sendConfirmationEmail(order: Order): void {
emailClient.send({
to: order.customerEmail,
subject: `Order #${order.id} Confirmed`,
body: `Your total is $${order.total}`
});
}
}
When the Marketing team redesigns the email template, you must touch OrderService. When the Database team migrates to a new ORM, you must touch OrderService. When the Product team changes pricing logic, you must touch OrderService. Each team risks breaking the other's code on every deploy.
The Fix: One Class, One Actor
// Axis 1: Business Logic only
class OrderCalculator {
calculateTotal(items: CartItem[]): number {
return items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
}
// Axis 2: Data Persistence only
class OrderRepository {
save(order: Order): void {
this.db.orders.create({ data: order });
}
}
// Axis 3: Notification only
class OrderNotifier {
sendConfirmation(order: Order): void {
this.emailClient.send(this.buildTemplate(order));
}
}
// Orchestrator: thin, delegates only
class OrderService {
constructor(
private calculator: OrderCalculator,
private repository: OrderRepository,
private notifier: OrderNotifier
) {}
async placeOrder(cart: Cart, customer: Customer): Promise<Order> {
const total = this.calculator.calculateTotal(cart.items);
const order = new Order(cart, customer, total);
await this.repository.save(order);
await this.notifier.sendConfirmation(order);
return order;
}
}
classDiagram
class OrderService {
+placeOrder(cart, customer) Order
}
class OrderCalculator {
+calculateTotal(items) number
}
class OrderRepository {
+save(order) void
}
class OrderNotifier {
+sendConfirmation(order) void
}
OrderService --> OrderCalculator
OrderService --> OrderRepository
OrderService --> OrderNotifier
B. Open-Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
Once a class has been tested and deployed to production, modifying it risks introducing regressions. OCP requires that new behavior be added by writing new code (extension) rather than modifying existing code.
The Problem: Modification-Driven Extension
// VIOLATION: Adding a new payment method requires modifying existing code
class PaymentProcessor {
process(payment: Payment): void {
if (payment.type === 'credit_card') {
this.stripeClient.charge(payment.amount, payment.token);
} else if (payment.type === 'paypal') {
this.paypalClient.execute(payment.amount, payment.email);
} else if (payment.type === 'crypto') { // <-- New requirement: must edit this class
this.coinbaseClient.transfer(payment.amount, payment.walletAddress);
}
}
}
Every new payment method requires touching PaymentProcessor, risking regressions in previously working payment flows.
The Fix: Extension Through Abstraction
// Abstraction (Stable β never changes)
interface PaymentStrategy {
process(payment: Payment): Promise<Receipt>;
}
// Extension: New payment methods are new classes β no modification
class StripeStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<Receipt> {
return this.stripeClient.charge(payment.amount, payment.token);
}
}
class PayPalStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<Receipt> {
return this.paypalClient.execute(payment.amount, payment.email);
}
}
class CryptoStrategy implements PaymentStrategy {
async process(payment: Payment): Promise<Receipt> {
return this.coinbaseClient.transfer(payment.amount, payment.walletAddress);
}
}
// Closed to modification; open to extension via constructor injection
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
async process(payment: Payment): Promise<Receipt> {
return this.strategy.process(payment);
}
}
classDiagram
class PaymentStrategy {
<<interface>>
+process(payment) Receipt
}
class StripeStrategy {
+process(payment) Receipt
}
class PayPalStrategy {
+process(payment) Receipt
}
class CryptoStrategy {
+process(payment) Receipt
}
class PaymentProcessor {
-strategy: PaymentStrategy
+process(payment) Receipt
}
PaymentStrategy <|.. StripeStrategy
PaymentStrategy <|.. PayPalStrategy
PaymentStrategy <|.. CryptoStrategy
PaymentProcessor --> PaymentStrategy
C. Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types without altering the correctness of the program."
LSP, formalized by Barbara Liskov in 1987, is a behavioral contract principle. It is not about syntax inheritance β it is about behavioral guarantees. A subclass must honor the pre-conditions, post-conditions, and invariants of its parent.
The Classic Violation: The Square-Rectangle Problem
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(w: number): void { this.width = w; }
setHeight(h: number): void { this.height = h; }
area(): number { return this.width * this.height; }
}
class Square extends Rectangle {
// VIOLATION: Overrides parent contract β setting width also changes height
setWidth(w: number): void {
this.width = w;
this.height = w; // Breaks parent invariant: width and height are independent
}
setHeight(h: number): void {
this.width = h;
this.height = h;
}
}
// Consumer code that breaks with Square
function assertRectangleBehavior(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(10);
// Expected: 50. If rect is a Square: area = 100. CONTRACT VIOLATED.
console.assert(rect.area() === 50, 'Area must be width Γ height');
}
assertRectangleBehavior(new Rectangle(0, 0)); // PASSES
assertRectangleBehavior(new Square(0, 0)); // FAILS β substitution breaks correctness
The Fix: Model Behavior, Not Taxonomy
// Use a common interface for things that can compute area
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number { return this.width * this.height; }
}
class Square implements Shape {
constructor(private side: number) {}
area(): number { return this.side * this.side; }
}
Squares and Rectangles share the Shape contract. Neither is a subtype of the other β they simply share a common behavioral abstraction. No substitution contract is violated.
Real-World LSP Violation: The Read-Only Repository
class UserRepository {
findById(id: string): User { /* ... */ }
save(user: User): void { /* ... */ } // Base contract: supports writes
delete(id: string): void { /* ... */ }
}
// Violation: ReadOnlyUserRepository cannot honor the save/delete contract
class ReadOnlyUserRepository extends UserRepository {
save(user: User): void {
throw new Error('This repository is read-only'); // VIOLATES LSP
}
delete(id: string): void {
throw new Error('This repository is read-only'); // VIOLATES LSP
}
}
The fix is to separate the read and write interfaces at the design level:
interface ReadableUserRepository {
findById(id: string): Promise<User>;
findAll(): Promise<User[]>;
}
interface WritableUserRepository extends ReadableUserRepository {
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
D. Interface Segregation Principle (ISP)
"Clients should not be forced to depend on interfaces they do not use."
Fat interfaces create hidden coupling. If a class implements a large interface, it must provide implementations for all methods β even those irrelevant to its role. When that interface changes, all implementing classes must be updated.
The Problem: The Bloated Interface
// VIOLATION: A single interface for all worker operations
interface Worker {
work(): void;
eat(): void;
sleep(): void;
attendMeeting(): void;
}
// Human worker: naturally implements all methods
class HumanWorker implements Worker {
work(): void { console.log('Working...'); }
eat(): void { console.log('Eating lunch...'); }
sleep(): void { console.log('Sleeping...'); }
attendMeeting(): void { console.log('In meeting...'); }
}
// Robot worker: forced to implement irrelevant methods
class RobotWorker implements Worker {
work(): void { console.log('Processing task...'); }
eat(): void { throw new Error('Robots do not eat'); } // Forced stub
sleep(): void { throw new Error('Robots do not sleep'); } // Forced stub
attendMeeting(): void { throw new Error('Robots cannot attend meetings'); } // Forced stub
}
The Fix: Role-Specific Interfaces
// Segregated interfaces β each models one behavioral role
interface Workable {
work(): void;
}
interface Feedable {
eat(): void;
}
interface Restable {
sleep(): void;
}
interface MeetingAttendee {
attendMeeting(): void;
}
// Human implements all roles it naturally performs
class HumanWorker implements Workable, Feedable, Restable, MeetingAttendee {
work(): void { console.log('Working...'); }
eat(): void { console.log('Eating lunch...'); }
sleep(): void { console.log('Sleeping...'); }
attendMeeting(): void { console.log('In meeting...'); }
}
// Robot implements only the roles it can perform
class RobotWorker implements Workable {
work(): void { console.log('Processing task...'); }
}
// Consumer code depends only on what it needs
function runProductionShift(workers: Workable[]): void {
workers.forEach(w => w.work());
}
ISP in API Design: GraphQL vs REST
ISP is the theoretical foundation for GraphQL's existence. REST APIs often return "fat responses" β entire resource representations β forcing clients to receive fields they don't need (over-fetching). GraphQL applies ISP at the API layer: each client declares exactly the fields it requires.
REST (ISP Violation): GraphQL (ISP Compliant):
GET /api/user/123 query { user(id: "123") { name avatar } }
Returns: { id, name, email, avatar,
address, phone, preferences,
paymentMethods, orderHistory } Returns: { name, avatar }
E. Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."
DIP inverts the traditional direction of dependency. Normally, high-level orchestration code imports and directly instantiates low-level utility code. DIP requires that the dependency arrow points in the opposite direction β toward an abstraction owned by the high-level layer.
The Traditional (Inverted) Dependency
[High-Level: OrderService] ββdepends onββ> [Low-Level: MySQLOrderRepository]
OrderService must import MySQLOrderRepository. If the team migrates to PostgreSQL, OrderService must be modified. The high-level business logic is coupled to a low-level infrastructure detail.
The DIP-Compliant Dependency
// Abstraction: owned by the high-level domain layer
// This interface lives in the domain package, NOT the infrastructure package
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
findByCustomerId(customerId: string): Promise<Order[]>;
}
// High-level module: depends only on the abstraction
class OrderService {
constructor(private orderRepository: OrderRepository) {}
async placeOrder(cart: Cart, customer: Customer): Promise<Order> {
const order = Order.create(cart, customer);
await this.orderRepository.save(order);
return order;
}
}
// Low-level detail: depends on (implements) the abstraction
class PrismaOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
await prisma.order.create({ data: order.toDataModel() });
}
async findById(id: string): Promise<Order | null> {
const data = await prisma.order.findUnique({ where: { id } });
return data ? Order.fromDataModel(data) : null;
}
async findByCustomerId(customerId: string): Promise<Order[]> {
const data = await prisma.order.findMany({ where: { customerId } });
return data.map(Order.fromDataModel);
}
}
// Dependency injection: wire everything together at the composition root
const orderService = new OrderService(new PrismaOrderRepository());
classDiagram
class OrderRepository {
<<interface>>
+save(order) void
+findById(id) Order
}
class OrderService {
-repo: OrderRepository
+placeOrder(cart, customer) Order
}
class PrismaOrderRepository {
+save(order) void
+findById(id) Order
}
OrderService --> OrderRepository
PrismaOrderRepository ..|> OrderRepository
note for PrismaOrderRepository "The arrow points FROM infrastructure TO domain. Dependency is inverted."
The dependency arrow now points from the low-level infrastructure layer inward toward the high-level domain layer. The OrderService no longer knows that Prisma or PostgreSQL exist.
Section 2: SOLID in Practice β Full Refactoring Case Study
The Before: A Tangled OrderService
// Before SOLID: all concerns mixed in one class (236 lines in production)
class OrderService {
private db = new MySQL();
private mailer = new SendGridClient(process.env.SENDGRID_KEY);
private logger = new FileLogger('/var/log/orders.log');
placeOrder(customerId: string, items: any[]): any {
// Validation
if (!items || items.length === 0) throw new Error('Empty cart');
// Pricing (hardcoded logic)
let total = 0;
for (const item of items) {
if (item.category === 'electronics') total += item.price * 0.9;
else total += item.price;
}
// Persistence (direct SQL)
const orderId = this.db.query(
`INSERT INTO orders (customer_id, total) VALUES (${customerId}, ${total})`
);
// Email (direct SMTP call)
this.mailer.send({
to: this.db.query(`SELECT email FROM customers WHERE id = ${customerId}`)[0].email,
subject: 'Order Confirmed',
body: `Order ${orderId} placed. Total: $${total}`
});
// Logging
this.logger.write(`Order ${orderId} placed by customer ${customerId}`);
return { orderId, total };
}
}
Problems:
- SRP violated: 5 responsibilities (validation, pricing, persistence, notification, logging).
- OCP violated: Adding a new discount category requires modifying
placeOrder. - DIP violated: Directly instantiates
MySQL,SendGridClient, andFileLogger. - Untestable: Cannot unit test pricing without a real database.
The After: SOLID-Compliant Design
// Interfaces (Domain Layer β stable abstractions)
interface PricingStrategy { calculate(items: CartItem[]): number; }
interface OrderRepo { save(order: Order): Promise<string>; findCustomerEmail(id: string): Promise<string>; }
interface Notifier { sendOrderConfirmation(orderId: string, email: string, total: number): Promise<void>; }
// Pricing strategies (OCP: add new ones without modifying existing)
class StandardPricing implements PricingStrategy {
calculate(items: CartItem[]): number {
return items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
}
class ElectronicsDiscountPricing implements PricingStrategy {
calculate(items: CartItem[]): number {
return items.reduce((sum, i) =>
sum + i.price * i.quantity * (i.category === 'electronics' ? 0.9 : 1.0), 0);
}
}
// Service: orchestrates, depends on abstractions only (DIP)
class OrderService {
constructor(
private pricing: PricingStrategy, // injected
private repo: OrderRepo, // injected
private notifier: Notifier // injected
) {}
async placeOrder(customerId: string, items: CartItem[]): Promise<{ orderId: string; total: number }> {
if (!items?.length) throw new Error('Cart cannot be empty');
const total = this.pricing.calculate(items);
const order = new Order(customerId, items, total);
const orderId = await this.repo.save(order);
const email = await this.repo.findCustomerEmail(customerId);
await this.notifier.sendOrderConfirmation(orderId, email, total);
return { orderId, total };
}
}
Now testable in isolation:
// Unit test: zero database, zero email, zero network
describe('OrderService', () => {
it('applies electronics discount correctly', async () => {
const mockRepo: OrderRepo = {
save: jest.fn().mockResolvedValue('order-123'),
findCustomerEmail: jest.fn().mockResolvedValue('test@example.com')
};
const mockNotifier: Notifier = { sendOrderConfirmation: jest.fn() };
const service = new OrderService(
new ElectronicsDiscountPricing(),
mockRepo,
mockNotifier
);
const result = await service.placeOrder('cust-1', [
{ name: 'Laptop', price: 1000, quantity: 1, category: 'electronics' }
]);
expect(result.total).toBe(900); // 10% discount applied
});
});
Section 3: Static Code Analysis Tools
A. SonarQube
SonarQube is an automated code quality platform that analyzes source code for bugs, code smells, security vulnerabilities, and maintainability issues. It maps directly to SOLID violations:
| SonarQube Rule | SOLID Principle Violated |
|---|---|
S1448 β Too many methods in a class |
SRP |
S138 β Functions should not have too many lines |
SRP |
S2176 β Class names should not shadow interfaces |
OCP |
S3740 β Raw types should not be used |
ISP / DIP |
S4830 β Server certificates should be verified |
Security (DIP β depend on verified contracts) |
SonarQube Quality Gate Configuration:
# sonar-project.properties
sonar.projectKey=macro-patterns-api
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.spec.ts
sonar.coverage.exclusions=**/generated/**
sonar.qualitygate.wait=true
# Quality Gate thresholds
sonar.coverage.minimum=80
sonar.duplicated_lines_density.maximum=3
sonar.cognitive_complexity.maximum=15
B. ESLint with Architecture Rules
For TypeScript projects, ESLint with eslint-plugin-boundaries enforces import directionality:
{
"rules": {
"boundaries/element-types": ["error", {
"default": "disallow",
"rules": [
{ "from": "infrastructure", "allow": ["domain"] },
{ "from": "application", "allow": ["domain"] },
{ "from": "domain", "allow": [] }
]
}]
}
}
This rule enforces DIP at the import level: infrastructure and application layers can import from domain, but domain cannot import from infrastructure. Violating this rule fails CI.
Section 4: Interface Contracts & Component Design
A. Defining Interface Contracts
An interface contract is a precise specification of the behavioral obligations a component commits to upholding. A contract consists of:
- Pre-conditions: What must be true before calling the method.
- Post-conditions: What is guaranteed to be true after the method returns.
- Invariants: What remains constant throughout the component's lifecycle.
/**
* Contract:
* Pre-condition: orderId must be a non-empty UUID string.
* Post-condition: Returns the Order with matching id, or null if not found.
* Never throws for a missing record (throws only for invalid input).
* Invariant: The returned Order's id always equals the input orderId.
*/
interface OrderRepository {
findById(orderId: string): Promise<Order | null>;
}
B. Local Component Interface Contracts (Documentation Deliverable)
The Local Component Interface Contract is a structured documentation artifact created for each bounded service boundary. It answers:
| Section | Content |
|---|---|
| Component Name | OrderService |
| Responsibilities | Orchestrate order placement flow |
| Public Interface | placeOrder(customerId, items): Promise<OrderResult> |
| Dependencies (Abstractions) | PricingStrategy, OrderRepository, Notifier |
| Pre-conditions | items must be non-empty; customerId must be valid UUID |
| Post-conditions | Order persisted, confirmation sent, idempotent on retry |
| Error Modes | CartEmptyError, CustomerNotFoundError, PersistenceError |
| Test Coverage Requirement | 90% branch coverage |
Section 5: Hands-On Practice Challenge
The Challenge
The diagram below shows the current tight coupling between OrderService and PaymentGateway. This architecture violates the Dependency Inversion Principle.
classDiagram
class OrderService {
-gateway: PaymentGateway
+processOrder(order) void
}
class PaymentGateway {
+charge(amount, token) Receipt
}
OrderService --> PaymentGateway : direct dependency
Your Goal
Refactor this diagram to introduce a PaymentService interface, making OrderService depend on the abstraction and PaymentGateway implement it.
Target architecture:
OrderService ββ(depends on)ββ> PaymentService (interface)
^
| (implements)
PaymentGateway
Solution Model
classDiagram
class PaymentService {
<<interface>>
+charge(amount, token) Receipt
+refund(receiptId) void
}
class OrderService {
-paymentService: PaymentService
+processOrder(order) void
}
class PaymentGateway {
+charge(amount, token) Receipt
+refund(receiptId) void
}
class StripeGateway {
+charge(amount, token) Receipt
+refund(receiptId) void
}
OrderService --> PaymentService
PaymentGateway ..|> PaymentService
StripeGateway ..|> PaymentService
Key observations:
OrderServicenow depends only on thePaymentServiceinterface.- Both
PaymentGatewayandStripeGatewayimplement the same contract. - To switch payment providers, inject a different implementation at the composition root β
OrderServicecode does not change (OCP satisfied).
Section 6: SOLID Principle Summary
| Principle | Core Rule | Primary Risk It Prevents | Key Tool |
|---|---|---|---|
| SRP | One reason to change | Regression blast radius | Split classes by actor |
| OCP | Extend without modifying | Production regressions | Interfaces + Strategy |
| LSP | Honor parent contracts | Silent behavioral bugs | Design for behavior, not taxonomy |
| ISP | Depend on what you use | Unnecessary recompilation | Role-specific interfaces |
| DIP | Depend on abstractions | Infrastructure lock-in | Constructor injection |
Bridge to Module 2: The SOLID principles define the rules. The Gang of Four Design Patterns (Module 2) provide the reusable structural blueprints that apply these rules to recurring software design problems.