Solution Architecture Practice

Comprehensive guide to software architecture frameworks, patterns, and best practices

View project on GitHub

Modularity and Loose Coupling Principle

Overview

Principle: Systems must be organized into independent, cohesive modules with clear boundaries and minimal interdependencies.

Modularity enables independent development, testing, and deployment. Loose coupling enables changing one component without cascading changes.

Rationale

  • Tight coupling creates brittle systems
  • Monolithic architectures slow development
  • Changes propagate through system
  • Teams step on each other
  • Independent deployment impossible
  • Modularity enables scaling teams

Core Implications

1. Domain-Driven Design (DDD)

Bounded Contexts:

Company
├─ Sales Bounded Context
│   ├─ Customer aggregate
│   ├─ Order aggregate
│   └─ Quote aggregate
│
├─ Fulfillment Bounded Context
│   ├─ Shipment aggregate
│   ├─ Warehouse aggregate
│   └─ Inventory aggregate
│
└─ Finance Bounded Context
    ├─ Invoice aggregate
    ├─ Payment aggregate
    └─ Revenue aggregate

Bounded Context Characteristics:

  • Clear responsibility
  • Defined boundary
  • Own domain model
  • Explicit interfaces
  • Independent development

2. Coupling Types

Temporal Coupling:

❌ Bad - Order creation blocks until inventory updated
Order Service calls Inventory Service synchronously
→ If Inventory Service slow/down, orders fail

✅ Good - Order created, Inventory updated asynchronously
Order Service publishes "OrderCreated" event
Inventory Service subscribes to event
→ Decoupled, each can fail independently

Data Coupling:

❌ Bad - Services share database
Service A → Database ← Service B
→ Schema change breaks both services

✅ Good - Each service owns data
Service A → Database A
Service B → Database B
→ Independent evolution

Control Coupling:

❌ Bad - Service A controls Service B logic
Service A: "Do X, then Y, then Z"
Service B: Just follows orders

✅ Good - Services collaborate via contracts
Service A: "I need X done"
Service B: "I'll do X using my logic"
→ Services hide implementation

3. Service Mesh and Communication

Module Communication Patterns:

Synchronous:
User Request → API Gateway → Service A → Service B → Response
(Tight coupling on availability)

Asynchronous (Event-Driven):
Service A: Publishes "Event"
Service B: Subscribes to "Event", processes when ready
(Loose coupling on timing)

Asynchronous (Request-Reply):
Service A: Sends Request, continues
Service B: Processes, sends Reply when ready
(Decoupled timing, but expects response)

Service Communication Contract:

Service: Order Service
Endpoint: POST /orders
Request:
  {
    "customer_id": "string",
    "items": [
      { "product_id": "string", "quantity": "number" }
    ]
  }
Response (200):
  {
    "order_id": "string",
    "status": "pending",
    "total": "number"
  }
Error Responses:
  400: Invalid input
  404: Customer not found
  409: Item out of stock

4. Module Boundaries

Bad Module Organization:

OrderService:
├─ Order logic
├─ Payment logic
├─ Notification logic
├─ Tax calculation logic
├─ Shipping logic
└─ Reporting logic

All tightly coupled, difficult to test, change everything at once

Good Module Organization:

OrderService:
├─ Order logic
└─ Domain events

PaymentService:
├─ Payment logic
└─ Domain events

NotificationService:
├─ Notification logic
└─ Subscription to domain events

TaxService:
├─ Tax calculation
└─ Exposed via API

ShippingService:
├─ Shipping logic
└─ Subscription to domain events

ReportingService:
├─ Reporting logic
└─ Subscription to domain events (event sourcing)

Implementation Practices

1. Explicit Service Contracts

// Clear, versioned service contract

interface OrderServiceAPI {
  version: "v1"
  
  // Create order
  POST("/v1/orders", CreateOrderRequest): Promise<OrderResponse>
  
  // Get order
  GET("/v1/orders/{orderId}"): Promise<OrderResponse>
  
  // Cancel order
  POST("/v1/orders/{orderId}/cancel", CancelRequest): Promise<OrderResponse>
  
  // Events published
  events: {
    "order.created": OrderCreatedEvent,
    "order.confirmed": OrderConfirmedEvent,
    "order.cancelled": OrderCancelledEvent
  }
}

// Contract changes require versioning strategy
// v1 endpoints stay stable, v2 for breaking changes

2. Event-Driven Architecture

┌──────────────────────┐
│   OrderService       │
├──────────────────────┤
│ Creates Order        │
│ Publishes            │
│ order.created event  │
└────────┬─────────────┘
         │
         │ order.created event
         │
         ├─────────────────────────┬──────────────────┬─────────────────┐
         │                         │                  │                 │
    ┌────▼──────────┐      ┌──────▼────────┐  ┌──────▼──────────┐  ┌──▼──────────┐
    │Inventory      │      │Notification   │  │Reporting        │  │Finance      │
    │Service        │      │Service        │  │Service          │  │Service      │
    │               │      │               │  │                 │  │             │
    │Reserves stock │      │Emails         │  │Records order    │  │Creates      │
    │Updates cache  │      │notification   │  │for revenue      │  │invoice      │
    └───────────────┘      └───────────────┘  └─────────────────┘  └─────────────┘

Event Structure:

{
  "eventId": "evt-123456",
  "eventType": "order.created",
  "timestamp": "2025-11-12T10:30:45.123Z",
  "aggregateId": "order-789",
  "aggregateType": "Order",
  "version": 1,
  "data": {
    "orderId": "order-789",
    "customerId": "customer-456",
    "items": [
      {
        "productId": "product-123",
        "quantity": 2,
        "price": 99.99
      }
    ],
    "total": 199.98
  },
  "metadata": {
    "userId": "user-456",
    "ipAddress": "192.168.1.1",
    "source": "web"
  }
}

3. Conway’s Law Alignment

Conway’s Law: “System design mirrors organization structure”

Bad Alignment:

Organization:        System:
Frontend Team → UI monolith ←→ Backend monolith ← Backend Team
                          (tight coupling)

Problem: Teams blocked by monolithic architecture

Good Alignment:

Organization:           System:
Order Team → Order Service (owns end-to-end)
Payment Team → Payment Service
Inventory Team → Inventory Service
(Clear ownership, independent teams, service architecture)

4. Testing Modularity

# Order Service Tests
├─ Unit Tests (Order logic)
│   ├─ Create order validation
│   ├─ Calculate totals
│   └─ Apply discounts
│
├─ Integration Tests (with Inventory via mock)
│   ├─ Reserve inventory on order creation
│   └─ Release inventory on cancellation
│
├─ Contract Tests (with Payment Service)
│   ├─ Payment API contract
│   └─ Event schema validation
│
└─ E2E Tests (via API, with all services)
    ├─ Create order with real services
    └─ Verify all services updated correctly

Each service tested independently, then integrated

Common Scenarios

Scenario 1: Adding Feature to Single Module

Situation: Need to add discount logic to Order Service.

Modular Approach:

  1. Change only Order Service
  2. Add discount calculation
  3. Test Order Service
  4. Deploy Order Service
  5. No other services affected

Monolithic Approach:

  1. Modify monolith
  2. Test entire monolith
  3. Potential side effects everywhere
  4. Deploy entire monolith
  5. Risk of breaking unrelated features

Scenario 2: Service Independence

Situation: Payment Service outage.

Loose Coupling:

  • Orders still created
  • Inventory reserved
  • Notifications sent
  • Payment Service catches up when restored

Tight Coupling:

  • Order creation fails
  • Cascading failure
  • Bad user experience

Scenario 3: Team Autonomy

Situation: Multiple teams developing features.

With Modules:

Team A (Order Service): Works independently
Team B (Payment Service): Works independently
Team C (Inventory Service): Works independently

Synchronized only at service boundaries (contracts)

Without Modules:

All teams: Modifying same codebase
Merging constantly: Conflicts everywhere
Blocking: Team B waits on Team A's changes

Risks of Ignoring This Principle

❌ Monolithic Complexity: Everything coupled to everything ❌ Deployment Bottleneck: Can’t deploy service without deploying all ❌ Team Contention: Developers constantly conflicting ❌ Change Propagation: Small change breaks multiple things ❌ Technology Lock: Can’t use different tech per service ❌ Scaling Issues: Can’t scale individual components

Best Practices

✅ Use bounded contexts (DDD) Clear ownership and responsibility.

✅ Define explicit contracts APIs, events, schemas should be versioned.

✅ Avoid shared data stores Each service owns its data.

✅ Use asynchronous communication Event-driven for loose coupling.

✅ Implement service discovery Services find each other dynamically.

✅ Use API versioning Support multiple versions during transition.

✅ Implement circuit breakers Prevent cascading failures.

✅ Monitor service health Know when services are down.

References

  • Domain-Driven Design (Eric Evans)
  • Building Microservices (Sam Newman)
  • Release It! (Michael Nygard) - Circuit breaker patterns

Last Updated: November 2025 Principle Category: Architecture Applies To: Services, applications, modules Related Documents: Service Architecture Guide, API Standards