Loading content...
If there is one universal truth in software development, it is this: requirements will change.
Users will request features no one anticipated. Markets will shift, requiring pivots. Regulations will mandate new behaviors. Bugs will reveal that assumptions were wrong. Technology will evolve, making current approaches obsolete. Integrations will be added, removed, or modified.
Change is not an edge case to be handled—it is the fundamental context in which all software exists. A system that cannot change is a system that will eventually fail.
Architecture is the structural foundation that either enables change or makes it prohibitively expensive. The true test of architectural quality is not how elegant the initial design is, but how gracefully the system accommodates the modifications that reality inevitably demands.
The measure of architectural quality is not static beauty—it's dynamic resilience. Can the system evolve? Can new features be added without rewriting existing ones? Can components be replaced? Can the system scale without restructuring? These questions reveal whether architecture serves its purpose.
Not all changes are equal. Understanding the different types of change helps us design architectures that handle them appropriately:
1. Feature Changes
New capabilities are added, existing ones are modified or removed. This is the most common type of change and the most visible to stakeholders.
Examples:
Architectural Implication: Features should be additive where possible. The system should have clear extension points that don't require modifying existing stable code.
2. Technology Changes
The tools, frameworks, databases, or platforms underlying the system change—by choice or necessity.
Examples:
Architectural Implication: Technology dependencies should be isolated behind abstractions. The core domain should be technology-agnostic.
3. Scale Changes
The system must handle significantly more load, data, or users than originally designed for.
Examples:
Architectural Implication: Systems should be designed with scaling seams—places where horizontal scaling can be applied without redesign.
4. Integration Changes
New external systems must be connected, or existing integrations must be modified or replaced.
Examples:
Architectural Implication: External integrations should be encapsulated behind stable internal interfaces. The system should be integration-agnostic.
5. Organizational Changes
Team structures, ownership, or development processes evolve.
Examples:
Architectural Implication: Architecture should enable Conway's Law to work in your favor—system boundaries should align with (or guide) team boundaries.
| Change Type | Good Architecture Response | Poor Architecture Response |
|---|---|---|
| Feature | Add new module; extend interface | Modify existing code everywhere |
| Technology | Swap implementation behind interface | Rewrite entire layers |
| Scale | Add instances; split at seams | Major restructuring required |
| Integration | Add adapter; implement interface | Changes propagate to core |
| Organizational | Reassign module ownership | Untangle shared code first |
The Open-Closed Principle (OCP) states that software entities should be open for extension but closed for modification. While typically applied at the class level, its architectural implications are profound:
A well-architected system can be extended with new behavior without changing existing, working code.
This principle, applied architecturally, means:
1. Plugin Architecture
The system defines extension points where new capabilities can be added. The core system provides infrastructure and interfaces; plugins provide specific implementations.
// Core defines interface
interface PaymentProcessor {
process(payment: Payment): Result;
}
// Extensions implement interface
class StripeProcessor implements PaymentProcessor { }
class PayPalProcessor implements PaymentProcessor { }
class CryptoProcessor implements PaymentProcessor { } // Added later!
// Adding CryptoProcessor required zero changes to existing code
2. Stable Abstractions
The abstractions that define boundaries should be stable—changing rarely if ever. Implementations can change freely behind them:
3. Protected Variation
Identify points of predicted variation and protect the system from the impact of changes in those areas:
You cannot close a system against all possible changes—that would require infinite abstraction. Instead, close strategically against the changes you anticipate. Identify the likely changes based on domain knowledge, business strategy, and historical patterns, then architect for those specific variations.
Coupling determines how changes propagate through a system. When components are tightly coupled, a change in one requires changes in others. When loosely coupled, changes are contained.
Types of Coupling (from worst to best):
The Change Ripple Effect
Poor architecture creates a ripple effect where changes cascade through the system:
What should have been a one-file change becomes a multi-day effort touching dozens of files.
Good Architecture Contains Changes
With loose coupling:
The change is contained at its source.
123456789101112131415161718192021222324252627282930313233343536
// TIGHT COUPLING: Internal details are exposedclass OrderService { private orders: Map<string, Order> = new Map(); getOrdersByCustomer(customerId: string): Order[] { // Callers depend on Order structure and Map behavior return Array.from(this.orders.values()) .filter(o => o.customerId === customerId); }} // Client is coupled to Order structure:const orders = orderService.getOrdersByCustomer('123');const total = orders.reduce((sum, o) => sum + o.items.length, 0);// If Order.items changes to Order.lineItems, this breaks! // ------------------------------------------- // LOOSE COUPLING: Details hidden behind stable interfaceinterface OrderSummary { orderId: string; itemCount: number; status: OrderStatus;} class OrderService { getOrderSummariesByCustomer(customerId: string): OrderSummary[] { // Internal implementation can change freely // Return type is a stable "view" that won't change often }} // Client depends only on stable interface:const summaries = orderService.getOrderSummariesByCustomer('123');const total = summaries.reduce((sum, s) => sum + s.itemCount, 0);// Internal Order changes don't affect clients!Cohesion describes how well the elements within a module belong together. High cohesion means the module has a single, well-defined purpose; low cohesion means it's a grab-bag of unrelated functionality.
Cohesion directly affects how change impacts the system:
High Cohesion → Localized Changes
When a module has a single responsibility, changes to that responsibility are confined to that module. You know where to go to make a change, and you know what else might be affected.
Low Cohesion → Scattered Changes
When a module mixes multiple responsibilities, a single conceptual change might require modifications in many modules that share the responsibility. Finding all the places that need to change is difficult.
The Single Responsibility Principle (SRP) at the Module Level
Robert C. Martin phrases SRP as: "A module should have one, and only one, reason to change." At the architectural level, this means:
UserModule contains:AuthModule - authentication onlyUserProfileUI - presentation onlyUserRepository - persistence onlyNotificationService - notificationsAnalyticsService - trackingWhat constitutes a 'single responsibility' depends on your domain and organization. In one company, 'user management' might be a coherent responsibility. In another, 'authentication' and 'profile management' might change for entirely different reasons and should be separate. Understand your domain's actual change patterns.
Abstraction layers are one of the most powerful tools for isolating change. By inserting layers of abstraction at strategic points, you create barriers that prevent changes from propagating:
How Abstraction Layers Work
Example: Database Abstraction
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// WITHOUT abstraction layer:// Business logic directly uses databaseclass OrderService { async getOrder(id: string) { const result = await pool.query( 'SELECT * FROM orders WHERE id = $1', // PostgreSQL specific [id] ); return result.rows[0]; // PostgreSQL response structure }} // If you switch databases, OrderService must change! // ------------------------------------------- // WITH abstraction layer:// Business logic uses repository interfaceinterface OrderRepository { findById(id: string): Promise<Order | null>;} class OrderService { constructor(private orderRepository: OrderRepository) {} async getOrder(id: string) { return this.orderRepository.findById(id); }} // Implementation handles database specificsclass PostgresOrderRepository implements OrderRepository { async findById(id: string): Promise<Order | null> { const result = await pool.query( 'SELECT * FROM orders WHERE id = $1', [id] ); return result.rows[0] ? this.mapToOrder(result.rows[0]) : null; }} // Switch to MongoDB: only change the implementationclass MongoOrderRepository implements OrderRepository { async findById(id: string): Promise<Order | null> { return await ordersCollection.findOne({ _id: id }); }} // OrderService never needs to change for database migrations!Layers of Isolation
Different abstraction layers protect against different types of changes:
| Layer | Protects Against |
|---|---|
| Repository | Database technology changes |
| Gateway/Adapter | External API changes |
| Presenter | UI framework changes |
| Use Case | Workflow changes |
| Entity | Domain concept changes |
The Cost of Abstraction
Abstraction isn't free. Each layer adds:
The key is strategic abstraction—abstracting at points where change is likely, not everywhere. Abstract too little and changes cascade. Abstract too much and the system becomes an indirection maze.
Modern software is rarely designed once and built to completion. Instead, it evolves continuously in response to feedback, changing requirements, and new opportunities. Evolutionary Architecture is an approach that embraces this reality:
An evolutionary architecture supports guided, incremental change across multiple dimensions.
Key Principles of Evolutionary Architecture:
1. Incremental Change
The architecture allows small, reversible changes rather than requiring large, risky transformations. This enables:
2. Fitness Functions
Define objective, measurable criteria for architectural health:
These functions are automated and run continuously, catching architectural drift early.
3. Last Responsible Moment
Defer irreversible decisions until you have the information to make them well. Early in a project:
Making premature decisions locks you into choices that may prove wrong. Instead, design the system so decisions can be deferred without blocking progress.
4. Sacrificial Architecture
Sometimes the best architecture for today is one you plan to throw away. This sounds counterintuitive, but consider:
Different stages have different architectural needs. What's right for a startup isn't right for an established product.
Evolutionary architecture only works if you have feedback mechanisms: comprehensive tests to catch regressions, monitoring to detect production issues, feature flags to control rollout, and the ability to roll back quickly. Without these safety nets, evolution becomes reckless experimentation.
Let's examine how different architectures respond to common change scenarios:
Scenario 1: Adding a New Payment Provider
Poor Architecture:
Good Architecture:
Scenario 2: Migrating from REST to GraphQL
Poor Architecture:
Good Architecture:
Scenario 3: Adding Multi-Tenancy
Poor Architecture:
Good Architecture:
These scenarios share a pattern: good architecture isolates concerns so that changes affect only the relevant portions. Poor architecture entangles concerns so that conceptually simple changes require widespread modifications. The difference is not cleverness—it's thoughtful separation.
We've reached the end of this module on Why Architecture Matters. Let's consolidate what we've learned about architecture and change:
Module Summary: Why Architecture Matters
Across these four pages, we've established the foundational case for investing in architecture:
What's Next
With this foundation, we're ready to explore specific architectural patterns. The next module examines Traditional Layered Architecture—the classic approach of organizing code into presentation, business, and data layers, with its benefits and limitations.
You now understand why architecture matters—not as abstract theory, but as the concrete structural decisions that determine whether your software thrives or struggles. This understanding provides the motivation and context for learning specific architectural patterns in the modules ahead.