Loading content...
A fast-growing e-commerce company hit a wall. Their single deployment—handling product catalog, inventory, orders, payments, shipping, and customer service—required 4-hour deploys. A bug in the payment module meant rolling back catalog updates. Black Friday traffic on the product pages crashed the order processing. The entire engineering team of 80 people worked in the same codebase, creating merge conflicts, stepping on each other's changes, and waiting in deployment queues.
The root cause wasn't that they had a monolith. The problem was that their monolith had no internal architecture—no clear boundaries, no separation of concerns, no single responsibilities at the system level.
SRP at the architecture level is about designing systems where major components have focused purposes, enabling independent evolution, scaling, and team ownership. Whether you build monoliths or microservices, architectural SRP is essential for sustainable systems.
By the end of this page, you will understand how SRP manifests at the architectural scale—how to identify service boundaries, design bounded contexts, structure subsystems for independent deployability, and make informed decisions about system decomposition. You'll see SRP as the guiding principle for scalable architecture.
At the architectural level, SRP can be stated as:
Each major system component (service, subsystem, bounded context) should have one clear business capability it provides and one cohesive reason to change.
This is the actor-based definition of SRP scaled up. Instead of asking 'which stakeholder drives changes to this class?', we ask:
When a service has multiple, unrelated reasons to change—different business domains, different stakeholders, different rate of change—it violates architectural SRP.
| Architecture Pattern | SRP Unit | SRP Question |
|---|---|---|
| Modular Monolith | Module/Package | Does this module represent one cohesive domain capability? |
| Microservices | Service | Does this service own one bounded context or business capability? |
| Event-Driven | Event Producer/Consumer | Does this component handle one category of domain events? |
| Layered Architecture | Layer | Does this layer handle one type of concern (presentation, business, data)? |
| Hexagonal/Ports & Adapters | Port/Adapter | Does each port represent one external interaction type? |
A strong signal for correct architectural SRP: Can one team own and operate this component independently? If a service requires constant coordination between 3 teams, it likely conflates multiple responsibilities. If one team can deliver features end-to-end within a service, boundaries are probably right.
Domain-Driven Design (DDD) provides a powerful framework for identifying architectural SRP boundaries through the concept of Bounded Contexts.
A Bounded Context is a boundary within which a particular domain model applies. Different bounded contexts may use different models, even for concepts with the same name.
Consider 'Customer':
These are different models with different data and behavior, owned by different teams, changing for different reasons. Trying to create a single 'Customer' that serves all contexts violates architectural SRP—any change ripples everywhere.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// EACH BOUNDED CONTEXT HAS ITS OWN MODEL // === SALES CONTEXT ===package com.company.sales; public class Customer { // Sales' view of Customer private CustomerId id; private ContactInfo contact; private List<PaymentMethod> paymentMethods; private PurchaseHistory history; private CustomerSegment segment; // Gold, Silver, Bronze public boolean isEligibleForDiscount(Promotion promo) { ... } public Money calculateLoyaltyDiscount() { ... }} // === SHIPPING CONTEXT ===package com.company.shipping; public class Customer { // Shipping's view of Customer private CustomerId id; private List<DeliveryAddress> addresses; private DeliveryPreferences preferences; // Leave at door, signature required private List<DeliveryInstruction> instructions; public DeliveryAddress getDefaultAddress() { ... } public boolean requiresSignature() { ... }} // === SUPPORT CONTEXT === package com.company.support; public class Customer { // Support's view of Customer private CustomerId id; private List<SupportTicket> ticketHistory; private SatisfactionScore csat; private CommunicationPreferences prefs; // Email, phone, chat public Priority calculateTicketPriority() { ... } public boolean isAtRisk() { ... } // Churn risk based on tickets} // CustomerId is the ONLY shared concept (anti-corruption layer)// Each context owns its complete model independentlyWhen bounded contexts need to interact, they do so through well-defined integration patterns: Shared Kernel (truly shared concepts), Customer-Supplier (one context serves another), Anti-Corruption Layer (translating between models), or Published Language (agreed-upon API contracts). These patterns preserve each context's SRP while enabling collaboration.
When decomposing a system into services (whether microservices or modules in a modular monolith), several criteria help identify SRP-compliant boundaries:
The Decomposition Decision Matrix:
When evaluating whether to split a component, consider multiple factors:
| Factor | Keep Together | Split Apart |
|---|---|---|
| Change Velocity | Change together at same rate | Change independently, different rates |
| Team Structure | Same team owns both | Different teams own each |
| Data Coupling | Share transactional data | Only need eventual consistency |
| Scaling Profile | Similar load patterns | Vastly different scaling needs |
| Deployment Risk | Must deploy atomically | Can deploy independently |
| Domain Cohesion | Same bounded context | Different bounded contexts |
The goal isn't maximum decomposition—it's appropriate decomposition. Too many services create distributed systems complexity: network latency, partial failures, data consistency challenges, debugging difficulty. Start with larger, cohesive services and split when pain emerges, not preemptively.
Architectural SRP violations create systemic problems that become increasingly painful as the system grows. Learning to recognize these patterns early prevents years of accumulated debt.
123456789101112131415161718192021222324252627282930
# DISTRIBUTED MONOLITH ANTI-PATTERN User Request Flow:┌──────────────────────────────────────────────────────────────────┐│ 1. API Gateway receives "Create Order" request ││ ↓ ││ 2. Order Service calls Product Service (sync) to validate items ││ ↓ ││ 3. Order Service calls Inventory Service (sync) to check stock ││ ↓ ││ 4. Order Service calls Pricing Service (sync) to calculate total││ ↓ ││ 5. Order Service calls User Service (sync) to get shipping addr ││ ↓ ││ 6. Order Service calls Payment Service (sync) to process payment││ ↓ ││ 7. Order Service calls Shipping Service (sync) to create shipment││ ↓ ││ 8. Order Service calls Notification Service (sync) to send email│└──────────────────────────────────────────────────────────────────┘ Problems:- 8 synchronous network calls for ONE operation- If ANY service is slow or down, entire operation fails- Latency = sum of all service latencies- Services can't be deployed independently (orchestration logic in Order Service)- This is a monolith distributed across network boundaries Root Cause: Services split by technical layer, not by business capabilityOrder Service has become a "God Service" orchestrating everythingIf testing one service requires spinning up 5 other services, you have coupling issues. A well-bounded service should be testable with stubs/mocks for its collaborators. If integration tests are the only way to verify behavior, boundaries may be wrong.
Event-driven architectures provide a natural way to maintain SRP at the architectural level by decoupling services through asynchronous events rather than synchronous calls. Each service becomes responsible for:
This preserves SRP because each service focuses on its bounded context and communicates through loosely-coupled events.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// EACH SERVICE HAS SINGLE RESPONSIBILITY + EVENTS // === ORDER SERVICE ===// Responsibility: Order lifecycle managementpublic class OrderService { private final EventPublisher events; public Order createOrder(CreateOrderCommand cmd) { // 1. Validate within OUR context (basic order rules) validateOrderRequest(cmd); // 2. Create order in PENDING state Order order = Order.create(cmd); orderRepository.save(order); // 3. Publish event - let other contexts react events.publish(new OrderCreated(order.getId(), order.getItems())); return order; // Order service is DONE - single responsibility } // React to external events @EventHandler public void on(PaymentCompleted event) { Order order = orderRepository.find(event.getOrderId()); order.confirmPayment(); orderRepository.save(order); events.publish(new OrderConfirmed(order.getId())); }} // === INVENTORY SERVICE ===// Responsibility: Stock managementpublic class InventoryService { @EventHandler public void on(OrderCreated event) { // React to orders by reserving stock for (OrderItem item : event.getItems()) { inventory.reserve(item.getProductId(), item.getQuantity()); } events.publish(new StockReserved(event.getOrderId())); }} // === PAYMENT SERVICE ===// Responsibility: Payment processingpublic class PaymentService { @EventHandler public void on(StockReserved event) { // Stock confirmed, now process payment PaymentResult result = processPayment(event.getOrderId()); events.publish(result.isSuccess() ? new PaymentCompleted(event.getOrderId()) : new PaymentFailed(event.getOrderId(), result.getReason())); }} // Benefits:// - Each service does ONE thing// - Services don't call each other synchronously// - Services can evolve independently// - If one service is slow, others aren't blocked// - Easy to add new services that react to eventsEvents become the API between bounded contexts. They must be designed as carefully as REST APIs: versioned, backward-compatible, and capturing business meaning. Events like 'OrderCreated' are stable; internal implementation can change freely.
Layered Architecture and Hexagonal Architecture (Ports & Adapters) are architectural patterns that embody SRP by separating concerns into distinct structural units.
Layered Architecture:
Traditional layers each have single responsibilities:
SRP is enforced by layer: UI changes don't affect domain logic; database changes don't affect presentation.
12345678910111213141516171819202122232425262728293031323334
# HEXAGONAL ARCHITECTURE (PORTS & ADAPTERS) ┌─────────────────────────────────────┐ │ ADAPTERS (Driving) │ │ REST GraphQL CLI Message │ │ Controller Resolver Cmd Handler │ └────────────────┬────────────────────┘ │ Ports (Interfaces) ▼ ┌───────────────────────────────────────────────────────┐ │ APPLICATION CORE │ │ ┌─────────────────────────────────────────────────┐ │ │ │ APPLICATION SERVICES │ │ │ │ Use Cases / Commands / Queries / Handlers │ │ │ └─────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ DOMAIN MODEL │ │ │ │ Entities / Value Objects / Domain Services │ │ │ │ Aggregates / Domain Events / Business Rules │ │ │ └─────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────┘ │ Ports (Interfaces) ▼ ┌─────────────────────────────────────┐ │ ADAPTERS (Driven) │ │ Database External Email File │ │ Repository API Service System│ └─────────────────────────────────────┘ SRP IN HEXAGONAL:- CORE has SINGLE responsibility: Business logic- Each ADAPTER has SINGLE responsibility: Translate one external concern- PORTS define contracts; implementations can be swapped- Core doesn't know about HTTP, SQL, or any infrastructureKey SRP Implications:
This structure enforces SRP at the architectural level within a single deployable unit—whether monolith or microservice.
In both layered and hexagonal architectures, dependencies point INWARD. Outer layers (adapters, presentation) depend on inner layers (domain, application). The core business logic never depends on infrastructure. This protects the domain's SRP—it changes only for business reasons, not technical ones.
Let's apply architectural SRP to a realistic e-commerce platform, showing how business capabilities map to service boundaries.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
# E-COMMERCE PLATFORM - SRP-DRIVEN DECOMPOSITION ┌─────────────────────────────────────────────────────────────────────────┐│ CUSTOMER-FACING DOMAIN │├─────────────────────┬─────────────────────┬────────────────────────────┤│ PRODUCT CATALOG │ SHOPPING CART │ SEARCH & DISCOVERY ││ ───────────────── │ ─────────────── │ ────────────────────── ││ • Product info │ • Cart management │ • Full-text search ││ • Categories │ • Pricing rules │ • Recommendations ││ • Attributes │ • Promo codes │ • Personalization ││ • Media assets │ • Cart persistence│ • Browse navigation ││ Team: Catalog │ Team: Shopping │ Team: Discovery │└─────────────────────┴─────────────────────┴────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐│ ORDER FULFILLMENT DOMAIN │├─────────────────────┬─────────────────────┬────────────────────────────┤│ ORDER MGMT │ INVENTORY │ SHIPPING ││ ───────────────── │ ─────────────── │ ────────────────────── ││ • Order lifecycle │ • Stock levels │ • Carrier integration ││ • Order history │ • Reservations │ • Rate calculation ││ • Returns/refunds │ • Reorder alerts │ • Tracking ││ • Order events │ • Warehouse sync │ • Delivery scheduling ││ Team: Orders │ Team: Inventory │ Team: Logistics │└─────────────────────┴─────────────────────┴────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐│ FINANCIAL DOMAIN │├─────────────────────┬─────────────────────┬────────────────────────────┤│ PAYMENTS │ BILLING │ TAX SERVICE ││ ───────────────── │ ─────────────── │ ────────────────────── ││ • Payment gateway │ • Invoice gen │ • Tax calculation ││ • Fraud detection │ • Subscription │ • Jurisdiction rules ││ • Refund process │ • Payment terms │ • Compliance ││ • PCI compliance │ • Revenue recog │ • Reporting ││ Team: Payments │ Team: Finance │ Team: Finance │└─────────────────────┴─────────────────────┴────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐│ CUSTOMER DOMAIN │├─────────────────────┬─────────────────────┬────────────────────────────┤│ IDENTITY │ PROFILE │ SUPPORT ││ ───────────────── │ ─────────────── │ ────────────────────── ││ • Authentication │ • User data │ • Ticket management ││ • Authorization │ • Preferences │ • Chat support ││ • Session mgmt │ • Order history │ • Knowledge base ││ • SSO integration │ • Saved items │ • Customer satisfaction ││ Team: Identity │ Team: Profile │ Team: Support │└─────────────────────┴─────────────────────┴────────────────────────────┘ WHY THESE BOUNDARIES:• Each service = one business capability• Each service = one team ownership• Different change velocities respected (Payments vs Catalog)• Different scaling needs separated (Search vs Orders)• Data ownership is clear (each service owns its data)This decomposition doesn't happen on day one. It evolves as the business and teams grow. Starting with a well-structured modular monolith that respects these boundaries internally makes later extraction into services much easier than retrofitting boundaries onto a tangled codebase.
Architectural SRP is where the principle has its greatest leverage. Get this right, and teams can work independently, systems can scale appropriately, and the organization can evolve without constant coordination overhead.
Module Complete:
We've now covered SRP at every scale—from individual methods through classes and modules to entire architectural components. SRP is a fractal principle: the same core idea (one reason to change, one actor served) applies at every level of software design. Master this, and you have a compass for countless design decisions.
You now understand SRP at all levels: methods should do one thing, classes should serve one actor, modules should represent one capability, and architectural components should own one bounded context. This completes your understanding of SRP at Different Levels. Apply this principle consistently, and your systems will be maintainable, testable, and evolvable for years to come.