Modular Architecture Mastery: Building Systems That Scale Without Breaking


Monoliths start fast and die slow. Microservices start slow and die fast. Modular systems start fast and scale forever. This guide reveals the architecture patterns that let you maintain startup velocity while building enterprise-grade reliability.

What you’ll master:

  • The Module Maturity Model: From spaghetti to symphony
  • Domain-Driven Design for real-world systems
  • The Dependency Inversion Principle that changes everything
  • Communication patterns: Events vs APIs vs Shared State
  • Real implementations with complete code examples
  • Migration strategies from monolith to modular
  • Case study: Scaling from 10 to 10M users with the same architecture

The Modular Architecture Spectrum

Understanding Your Current Position

type ArchitectureMaturity = {
  level: number;
  name: string;
  characteristics: string[];
  velocity: number; // Features per week
  reliability: number; // Uptime percentage
  scalability: string;
  teamSize: string;
};

const architectureSpectrum: ArchitectureMaturity[] = [
  {
    level: 0,
    name: 'Spaghetti Monolith',
    characteristics: [
      'No clear boundaries',
      'Everything depends on everything',
      'Global state everywhere',
      'Copy-paste programming'
    ],
    velocity: 10, // Fast initially
    reliability: 90,
    scalability: '100 users',
    teamSize: '1-2 developers'
  },
  {
    level: 1,
    name: 'Layered Architecture',
    characteristics: [
      'UI, Business, Data layers',
      'Some separation of concerns',
      'Shared database',
      'Synchronous communication'
    ],
    velocity: 8,
    reliability: 95,
    scalability: '1,000 users',
    teamSize: '3-5 developers'
  },
  {
    level: 2,
    name: 'Modular Monolith',
    characteristics: [
      'Clear module boundaries',
      'Internal APIs',
      'Module-specific data',
      'Can extract to services'
    ],
    velocity: 15,
    reliability: 99,
    scalability: '100,000 users',
    teamSize: '5-20 developers'
  },
  {
    level: 3,
    name: 'Service-Oriented',
    characteristics: [
      'Independent services',
      'API contracts',
      'Service-specific databases',
      'Async communication'
    ],
    velocity: 20,
    reliability: 99.9,
    scalability: '10M users',
    teamSize: '20-100 developers'
  },
  {
    level: 4,
    name: 'Domain-Driven Microservices',
    characteristics: [
      'Bounded contexts',
      'Event-driven',
      'Self-healing',
      'Platform abstractions'
    ],
    velocity: 30,
    reliability: 99.99,
    scalability: 'Unlimited',
    teamSize: '100+ developers'
  }
];

The Modular Sweet Spot

class ModularArchitecture {
  // The perfect balance: Monolith simplicity + Microservice flexibility
  
  advantages = {
    development: 'Single codebase, easy debugging',
    deployment: 'Single deployable, simple operations',
    performance: 'In-process calls, no network latency',
    flexibility: 'Can extract modules to services when needed',
    testing: 'Easy integration testing',
    refactoring: 'Safe large-scale changes'
  };
  
  principles = {
    'High Cohesion': 'Related functionality stays together',
    'Low Coupling': 'Modules interact through interfaces',
    'Clear Boundaries': 'Each module owns its data and logic',
    'Explicit Dependencies': 'No hidden connections',
    'Independent Development': 'Teams can work in parallel'
  };
}

Domain-Driven Design: The Foundation

Identifying Bounded Contexts

// Example: E-commerce platform bounded contexts
interface BoundedContext {
  name: string;
  responsibilities: string[];
  entities: string[];
  events: string[];
  commands: string[];
  queries: string[];
}

const ecommerceBoundedContexts: BoundedContext[] = [
  {
    name: 'Catalog',
    responsibilities: [
      'Product information management',
      'Category organization',
      'Search and filtering',
      'Inventory visibility'
    ],
    entities: ['Product', 'Category', 'Brand', 'Variant'],
    events: [
      'ProductCreated',
      'ProductUpdated',
      'ProductOutOfStock',
      'PriceChanged'
    ],
    commands: [
      'CreateProduct',
      'UpdateProduct',
      'UpdateInventory'
    ],
    queries: [
      'GetProduct',
      'SearchProducts',
      'GetCategories'
    ]
  },
  {
    name: 'Orders',
    responsibilities: [
      'Order processing',
      'Order fulfillment',
      'Order tracking',
      'Returns and refunds'
    ],
    entities: ['Order', 'OrderLine', 'Shipment', 'Return'],
    events: [
      'OrderPlaced',
      'OrderShipped',
      'OrderDelivered',
      'OrderCancelled'
    ],
    commands: [
      'PlaceOrder',
      'CancelOrder',
      'ShipOrder',
      'ProcessReturn'
    ],
    queries: [
      'GetOrder',
      'GetOrderHistory',
      'TrackShipment'
    ]
  },
  {
    name: 'Payments',
    responsibilities: [
      'Payment processing',
      'Payment methods',
      'Fraud detection',
      'Reconciliation'
    ],
    entities: ['Payment', 'PaymentMethod', 'Transaction', 'Refund'],
    events: [
      'PaymentAuthorized',
      'PaymentCaptured',
      'PaymentFailed',
      'RefundProcessed'
    ],
    commands: [
      'AuthorizePayment',
      'CapturePayment',
      'RefundPayment'
    ],
    queries: [
      'GetPaymentStatus',
      'GetTransactionHistory'
    ]
  }
];

Implementing Module Boundaries

// Module structure with clear boundaries
namespace CatalogModule {
  // Public API - What other modules can use
  export interface CatalogAPI {
    getProduct(id: string): Promise<Product>;
    searchProducts(criteria: SearchCriteria): Promise<Product[]>;
    checkInventory(productId: string): Promise<number>;
    reserveInventory(productId: string, quantity: number): Promise<boolean>;
  }
  
  // Internal implementation - Hidden from other modules
  class CatalogService implements CatalogAPI {
    constructor(
      private repository: CatalogRepository,
      private searchEngine: SearchEngine,
      private inventoryManager: InventoryManager,
      private eventBus: EventBus
    ) {}
    
    async getProduct(id: string): Promise<Product> {
      const product = await this.repository.findById(id);
      if (!product) {
        throw new ProductNotFoundError(id);
      }
      return this.mapToPublicProduct(product);
    }
    
    async reserveInventory(productId: string, quantity: number): Promise<boolean> {
      const reserved = await this.inventoryManager.reserve(productId, quantity);
      
      if (reserved) {
        await this.eventBus.publish(new InventoryReservedEvent({
          productId,
          quantity,
          timestamp: new Date()
        }));
      }
      
      return reserved;
    }
    
    // Private methods - not exposed
    private mapToPublicProduct(internal: InternalProduct): Product {
      // Transform internal representation to public API
      return {
        id: internal.id,
        name: internal.name,
        price: internal.currentPrice,
        available: internal.inventory > 0
      };
    }
  }
  
  // Module registration
  export function registerModule(container: DIContainer): void {
    container.register('CatalogAPI', CatalogService);
    container.register('CatalogRepository', PostgresCatalogRepository);
    container.register('SearchEngine', ElasticsearchEngine);
    container.register('InventoryManager', RedisInventoryManager);
  }
}

Communication Patterns Between Modules

1. Direct Method Calls (Synchronous)

class OrderService {
  constructor(
    private catalogAPI: CatalogAPI,
    private paymentsAPI: PaymentsAPI,
    private shippingAPI: ShippingAPI
  ) {}
  
  async placeOrder(request: PlaceOrderRequest): Promise<Order> {
    // Direct synchronous calls to other modules
    
    // Validate products exist and have inventory
    for (const item of request.items) {
      const product = await this.catalogAPI.getProduct(item.productId);
      const available = await this.catalogAPI.checkInventory(item.productId);
      
      if (available < item.quantity) {
        throw new InsufficientInventoryError(product.name);
      }
    }
    
    // Reserve inventory
    for (const item of request.items) {
      await this.catalogAPI.reserveInventory(item.productId, item.quantity);
    }
    
    // Process payment
    const payment = await this.paymentsAPI.authorizePayment({
      amount: this.calculateTotal(request.items),
      customerId: request.customerId,
      paymentMethod: request.paymentMethod
    });
    
    // Create order
    const order = await this.createOrder(request, payment.id);
    
    return order;
  }
}

2. Event-Driven Communication (Asynchronous)

// Event-driven architecture for loose coupling
class EventDrivenOrderService {
  constructor(
    private eventBus: EventBus,
    private orderRepository: OrderRepository
  ) {
    this.subscribeToEvents();
  }
  
  private subscribeToEvents(): void {
    this.eventBus.subscribe('ProductOutOfStock', this.handleOutOfStock.bind(this));
    this.eventBus.subscribe('PaymentFailed', this.handlePaymentFailed.bind(this));
    this.eventBus.subscribe('ShipmentDelayed', this.handleShipmentDelayed.bind(this));
  }
  
  async placeOrder(request: PlaceOrderRequest): Promise<Order> {
    // Create order in pending state
    const order = await this.orderRepository.create({
      ...request,
      status: OrderStatus.Pending
    });
    
    // Publish event - other modules will react
    await this.eventBus.publish(new OrderPlacedEvent({
      orderId: order.id,
      customerId: order.customerId,
      items: order.items,
      total: order.total
    }));
    
    return order;
  }
  
  // React to events from other modules
  private async handlePaymentAuthorized(event: PaymentAuthorizedEvent): Promise<void> {
    await this.orderRepository.updateStatus(event.orderId, OrderStatus.Confirmed);
    
    await this.eventBus.publish(new OrderConfirmedEvent({
      orderId: event.orderId,
      confirmedAt: new Date()
    }));
  }
  
  private async handleOutOfStock(event: ProductOutOfStockEvent): Promise<void> {
    // Find affected orders and handle appropriately
    const affectedOrders = await this.orderRepository.findByProduct(event.productId);
    
    for (const order of affectedOrders) {
      if (order.status === OrderStatus.Pending) {
        await this.cancelOrder(order.id, 'Product out of stock');
      }
    }
  }
}

3. Saga Pattern for Distributed Transactions

class OrderSaga {
  private steps: SagaStep[] = [];
  private compensations: CompensationStep[] = [];
  
  async execute(request: PlaceOrderRequest): Promise<Order> {
    try {
      // Step 1: Reserve inventory
      const inventoryReservation = await this.reserveInventory(request.items);
      this.compensations.push(() => this.releaseInventory(inventoryReservation));
      
      // Step 2: Authorize payment
      const paymentAuth = await this.authorizePayment(request.total);
      this.compensations.push(() => this.cancelPayment(paymentAuth));
      
      // Step 3: Create shipment
      const shipment = await this.createShipment(request.shippingAddress);
      this.compensations.push(() => this.cancelShipment(shipment));
      
      // Step 4: Create order
      const order = await this.createOrder(request);
      
      // All steps succeeded - saga complete
      return order;
      
    } catch (error) {
      // Something failed - run compensations in reverse order
      await this.compensate();
      throw new SagaFailedError('Order creation failed', error);
    }
  }
  
  private async compensate(): Promise<void> {
    for (const compensation of this.compensations.reverse()) {
      try {
        await compensation();
      } catch (error) {
        // Log compensation failure but continue
        console.error('Compensation failed:', error);
      }
    }
  }
}

The Dependency Inversion Principle

Ports and Adapters Architecture

// Core domain - no external dependencies
namespace Core {
  // Port (interface) - defined by the domain
  export interface UserRepository {
    save(user: User): Promise<void>;
    findById(id: string): Promise<User | null>;
    findByEmail(email: string): Promise<User | null>;
  }
  
  export interface EmailService {
    sendWelcomeEmail(user: User): Promise<void>;
    sendPasswordReset(email: string, token: string): Promise<void>;
  }
  
  // Domain service - depends only on interfaces
  export class UserService {
    constructor(
      private userRepository: UserRepository,
      private emailService: EmailService,
      private passwordHasher: PasswordHasher
    ) {}
    
    async createUser(request: CreateUserRequest): Promise<User> {
      // Check if user exists
      const existing = await this.userRepository.findByEmail(request.email);
      if (existing) {
        throw new UserAlreadyExistsError(request.email);
      }
      
      // Create user
      const user = new User({
        id: generateId(),
        email: request.email,
        passwordHash: await this.passwordHasher.hash(request.password),
        createdAt: new Date()
      });
      
      // Save to repository
      await this.userRepository.save(user);
      
      // Send welcome email
      await this.emailService.sendWelcomeEmail(user);
      
      return user;
    }
  }
}

// Infrastructure layer - implements the ports
namespace Infrastructure {
  // Adapter for PostgreSQL
  export class PostgresUserRepository implements Core.UserRepository {
    constructor(private db: Database) {}
    
    async save(user: User): Promise<void> {
      await this.db.query(
        'INSERT INTO users (id, email, password_hash, created_at) VALUES ($1, $2, $3, $4)',
        [user.id, user.email, user.passwordHash, user.createdAt]
      );
    }
    
    async findById(id: string): Promise<User | null> {
      const result = await this.db.query(
        'SELECT * FROM users WHERE id = $1',
        [id]
      );
      return result.rows[0] ? this.mapToUser(result.rows[0]) : null;
    }
  }
  
  // Adapter for MongoDB
  export class MongoUserRepository implements Core.UserRepository {
    constructor(private collection: Collection<UserDocument>) {}
    
    async save(user: User): Promise<void> {
      await this.collection.insertOne({
        _id: user.id,
        email: user.email,
        passwordHash: user.passwordHash,
        createdAt: user.createdAt
      });
    }
    
    async findById(id: string): Promise<User | null> {
      const doc = await this.collection.findOne({ _id: id });
      return doc ? this.mapToUser(doc) : null;
    }
  }
  
  // Adapter for SendGrid
  export class SendGridEmailService implements Core.EmailService {
    constructor(private sendgrid: SendGridClient) {}
    
    async sendWelcomeEmail(user: User): Promise<void> {
      await this.sendgrid.send({
        to: user.email,
        from: 'welcome@example.com',
        templateId: 'welcome-template',
        dynamicTemplateData: {
          name: user.name
        }
      });
    }
  }
}

Module Testing Strategies

Unit Testing Individual Modules

describe('CatalogModule', () => {
  let catalogService: CatalogService;
  let mockRepository: jest.Mocked<CatalogRepository>;
  let mockEventBus: jest.Mocked<EventBus>;
  
  beforeEach(() => {
    mockRepository = createMockRepository();
    mockEventBus = createMockEventBus();
    
    catalogService = new CatalogService(
      mockRepository,
      mockSearchEngine,
      mockInventoryManager,
      mockEventBus
    );
  });
  
  describe('reserveInventory', () => {
    it('should reserve inventory and publish event', async () => {
      // Arrange
      const productId = 'prod-123';
      const quantity = 5;
      mockInventoryManager.reserve.mockResolvedValue(true);
      
      // Act
      const result = await catalogService.reserveInventory(productId, quantity);
      
      // Assert
      expect(result).toBe(true);
      expect(mockInventoryManager.reserve).toHaveBeenCalledWith(productId, quantity);
      expect(mockEventBus.publish).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'InventoryReserved',
          data: { productId, quantity }
        })
      );
    });
    
    it('should not publish event if reservation fails', async () => {
      // Arrange
      mockInventoryManager.reserve.mockResolvedValue(false);
      
      // Act
      const result = await catalogService.reserveInventory('prod-123', 5);
      
      // Assert
      expect(result).toBe(false);
      expect(mockEventBus.publish).not.toHaveBeenCalled();
    });
  });
});

Integration Testing Between Modules

describe('Order-to-Payment Integration', () => {
  let orderService: OrderService;
  let paymentService: PaymentService;
  let testEventBus: TestEventBus;
  
  beforeEach(async () => {
    // Setup test containers with real modules
    const container = createTestContainer();
    
    orderService = container.get<OrderService>('OrderService');
    paymentService = container.get<PaymentService>('PaymentService');
    testEventBus = container.get<TestEventBus>('EventBus');
    
    await container.start();
  });
  
  it('should process payment when order is placed', async () => {
    // Arrange
    const orderRequest = createTestOrderRequest();
    
    // Act
    const order = await orderService.placeOrder(orderRequest);
    
    // Wait for async events to process
    await testEventBus.waitForEvent('PaymentAuthorized');
    
    // Assert
    const payment = await paymentService.getPayment(order.paymentId);
    expect(payment.status).toBe('authorized');
    expect(payment.amount).toBe(order.total);
  });
});

Contract Testing

// Ensure modules maintain their contracts
class ContractTest {
  @Contract('CatalogAPI.getProduct')
  async testGetProductContract(): Promise<void> {
    const catalogAPI = this.getCatalogAPI();
    
    // Test contract inputs
    await expect(catalogAPI.getProduct(null)).rejects.toThrow();
    await expect(catalogAPI.getProduct('')).rejects.toThrow();
    
    // Test contract outputs
    const product = await catalogAPI.getProduct('valid-id');
    expect(product).toMatchSchema({
      id: expect.any(String),
      name: expect.any(String),
      price: expect.any(Number),
      available: expect.any(Boolean)
    });
    
    // Test error contract
    await expect(catalogAPI.getProduct('non-existent'))
      .rejects.toThrow(ProductNotFoundError);
  }
}

Migration Strategy: From Monolith to Modular

Step-by-Step Migration

class MonolithToModularMigration {
  // Phase 1: Identify boundaries in existing code
  async phase1_identifyBoundaries(): Promise<ModuleBoundaries> {
    const codeAnalysis = await this.analyzeCodebase();
    
    return {
      modules: this.identifyModules(codeAnalysis),
      dependencies: this.mapDependencies(codeAnalysis),
      sharedData: this.findSharedData(codeAnalysis),
      crossCuttingConcerns: this.identifyCrossCutting(codeAnalysis)
    };
  }
  
  // Phase 2: Create module interfaces
  async phase2_createInterfaces(): Promise<void> {
    // Start with the least coupled module
    const targetModule = 'Authentication';
    
    // Define public API
    interface AuthenticationAPI {
      authenticate(credentials: Credentials): Promise<Token>;
      validateToken(token: string): Promise<boolean>;
      refreshToken(refreshToken: string): Promise<Token>;
    }
    
    // Create facade over existing code
    class AuthenticationFacade implements AuthenticationAPI {
      async authenticate(credentials: Credentials): Promise<Token> {
        // Delegate to existing monolith code
        return existingAuth.login(credentials.username, credentials.password);
      }
    }
  }
  
  // Phase 3: Extract module implementation
  async phase3_extractModule(): Promise<void> {
    // Copy relevant code to new module
    const moduleCode = await this.extractCode('Authentication');
    
    // Refactor to remove external dependencies
    await this.refactorToPureModule(moduleCode);
    
    // Add tests
    await this.addModuleTests(moduleCode);
    
    // Replace monolith code with calls to module
    await this.replaceWithModuleCalls();
  }
  
  // Phase 4: Separate data
  async phase4_separateData(): Promise<void> {
    // Create module-specific database/schema
    await this.createModuleDatabase('authentication');
    
    // Migrate data
    await this.migrateData({
      source: 'monolith.users',
      destination: 'authentication.users',
      transformation: this.transformUserData
    });
    
    // Setup sync during transition
    await this.setupDataSync();
  }
  
  // Phase 5: Complete extraction
  async phase5_completeExtraction(): Promise<void> {
    // Remove old code
    await this.removeMonolithCode('Authentication');
    
    // Update all references
    await this.updateReferences();
    
    // Remove data sync
    await this.removeDataSync();
    
    // Celebrate! 🎉
    console.log('Module extraction complete!');
  }
}

Strangler Fig Pattern Implementation

class StranglerFigPattern {
  private router: RequestRouter;
  private legacyApp: LegacyApplication;
  private modularApp: ModularApplication;
  
  async migrateGradually(): Promise<void> {
    // Start with all traffic to legacy
    this.router.route('/*', this.legacyApp);
    
    // Gradually move endpoints to new modules
    const endpoints = [
      '/api/auth/*',
      '/api/users/*',
      '/api/products/*',
      '/api/orders/*',
      '/api/payments/*'
    ];
    
    for (const endpoint of endpoints) {
      // Implement in new module
      await this.implementInModule(endpoint);
      
      // Test thoroughly
      await this.runTests(endpoint);
      
      // Route percentage of traffic
      for (const percentage of [5, 25, 50, 100]) {
        await this.router.split(endpoint, {
          legacy: 100 - percentage,
          modular: percentage
        });
        
        // Monitor for issues
        await this.monitor(endpoint, '24h');
        
        if (this.hasIssues(endpoint)) {
          await this.rollback(endpoint);
          break;
        }
      }
    }
  }
}

Real-World Case Study: Scaling to 10M Users

The Journey

const scalingJourney = {
  phase1: {
    name: 'MVP Launch',
    architecture: 'Rails Monolith',
    users: 1000,
    performance: '500ms response time',
    challenges: ['Slow tests', 'Deploy everything for small changes'],
    solution: 'Started identifying module boundaries'
  },
  
  phase2: {
    name: 'Product-Market Fit',
    architecture: 'Modular Monolith',
    users: 50000,
    performance: '200ms response time',
    challenges: ['Database bottleneck', 'Can\'t scale specific features'],
    solution: 'Extracted payment processing to separate module with own database'
  },
  
  phase3: {
    name: 'Rapid Growth',
    architecture: 'Hybrid (Monolith + Services)',
    users: 500000,
    performance: '100ms response time',
    challenges: ['Search too slow', 'Recommendations computationally expensive'],
    solution: 'Extracted search to Elasticsearch service, ML recommendations to separate service'
  },
  
  phase4: {
    name: 'Scale',
    architecture: 'Service-Oriented with Event Bus',
    users: 5000000,
    performance: '50ms response time',
    challenges: ['Complex deployments', 'Service coordination'],
    solution: 'Implemented saga pattern, service mesh for communication'
  },
  
  phase5: {
    name: 'Platform',
    architecture: 'Domain-Driven Microservices',
    users: 10000000,
    performance: '25ms response time',
    challenges: ['Platform complexity', 'Developer experience'],
    solution: 'Platform team provides abstractions, self-service tools'
  }
};

Key Lessons

const lessonsLearned = {
  'Start Modular': 'Even in monolith, maintain module boundaries',
  'Extract When Needed': 'Don\'t extract to services prematurely',
  'Data Is Hardest': 'Separating data is harder than separating code',
  'Events Over APIs': 'Event-driven is more flexible than synchronous APIs',
  'Invest In Tools': 'Good tooling makes modularity sustainable',
  'Conway\'s Law': 'Organize teams around modules/services',
  'Gradual Migration': 'Big bang rewrites usually fail'
};

Performance Optimization in Modular Systems

Avoiding the Distributed Monolith

class PerformanceOptimizer {
  // Common anti-pattern: Too many synchronous calls
  async antiPattern_chattyCommunication(orderId: string): Promise<OrderDetails> {
    const order = await this.orderService.getOrder(orderId);
    const customer = await this.customerService.getCustomer(order.customerId);
    
    const items = [];
    for (const item of order.items) {
      const product = await this.catalogService.getProduct(item.productId);
      items.push({ ...item, product });
    }
    
    const payment = await this.paymentService.getPayment(order.paymentId);
    const shipping = await this.shippingService.getShipment(order.shipmentId);
    
    // 5+ network calls = slow!
    return { order, customer, items, payment, shipping };
  }
  
  // Solution 1: Batch requests
  async solution1_batchRequests(orderId: string): Promise<OrderDetails> {
    const order = await this.orderService.getOrder(orderId);
    
    // Parallel requests
    const [customer, products, payment, shipping] = await Promise.all([
      this.customerService.getCustomer(order.customerId),
      this.catalogService.getProducts(order.items.map(i => i.productId)),
      this.paymentService.getPayment(order.paymentId),
      this.shippingService.getShipment(order.shipmentId)
    ]);
    
    return { order, customer, products, payment, shipping };
  }
  
  // Solution 2: Materialized views
  async solution2_materializedView(orderId: string): Promise<OrderDetails> {
    // Pre-computed view with all data
    return await this.orderViewService.getOrderDetails(orderId);
  }
  
  // Solution 3: GraphQL aggregation
  async solution3_graphQL(orderId: string): Promise<OrderDetails> {
    const query = `
      query GetOrderDetails($orderId: ID!) {
        order(id: $orderId) {
          id
          total
          customer {
            name
            email
          }
          items {
            product {
              name
              price
            }
            quantity
          }
          payment {
            status
            method
          }
        }
      }
    `;
    
    return await this.graphqlGateway.query(query, { orderId });
  }
}

Observability and Debugging

Distributed Tracing

class DistributedTracing {
  async traceModularRequest(request: Request): Promise<Response> {
    // Create root span
    const span = this.tracer.startSpan('http.request', {
      attributes: {
        'http.method': request.method,
        'http.url': request.url,
        'http.target': request.path
      }
    });
    
    try {
      // Pass trace context to modules
      const context = { traceId: span.traceId, spanId: span.spanId };
      
      // Module calls create child spans
      const authSpan = this.tracer.startSpan('auth.validate', { parent: span });
      const auth = await this.authModule.validate(request.token, context);
      authSpan.end();
      
      const dataSpan = this.tracer.startSpan('data.fetch', { parent: span });
      const data = await this.dataModule.fetch(request.params, context);
      dataSpan.end();
      
      span.setStatus({ code: SpanStatusCode.OK });
      return { status: 200, data };
      
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw error;
    } finally {
      span.end();
    }
  }
}

Module Health Checks

interface ModuleHealth {
  name: string;
  status: 'healthy' | 'degraded' | 'unhealthy';
  latency: number;
  errorRate: number;
  dependencies: DependencyHealth[];
}

class HealthMonitor {
  async checkSystemHealth(): Promise<SystemHealth> {
    const modules = await Promise.all([
      this.checkModule('Catalog'),
      this.checkModule('Orders'),
      this.checkModule('Payments'),
      this.checkModule('Shipping')
    ]);
    
    const overallStatus = this.calculateOverallStatus(modules);
    
    return {
      status: overallStatus,
      modules,
      timestamp: new Date(),
      recommendations: this.generateRecommendations(modules)
    };
  }
  
  private async checkModule(name: string): Promise<ModuleHealth> {
    const module = this.getModule(name);
    
    const [health, metrics, dependencies] = await Promise.all([
      module.checkHealth(),
      module.getMetrics(),
      module.checkDependencies()
    ]);
    
    return {
      name,
      status: health.status,
      latency: metrics.p95Latency,
      errorRate: metrics.errorRate,
      dependencies
    };
  }
}

Conclusion: The Path to Modular Mastery

Modular architecture isn’t just about organizing code—it’s about creating systems that can evolve, scale, and thrive under changing requirements and growing teams.

Your Modular Architecture Roadmap

const modularRoadmap = {
  week1: {
    focus: 'Identify module boundaries',
    actions: [
      'Map current architecture',
      'Identify bounded contexts',
      'Find shared data and dependencies'
    ]
  },
  
  month1: {
    focus: 'Create first module',
    actions: [
      'Define module API',
      'Extract implementation',
      'Add comprehensive tests',
      'Setup module-specific monitoring'
    ]
  },
  
  month3: {
    focus: 'Establish patterns',
    actions: [
      'Standardize module structure',
      'Implement event bus',
      'Create module template',
      'Document best practices'
    ]
  },
  
  month6: {
    focus: 'Scale and optimize',
    actions: [
      'Extract critical modules to services',
      'Implement caching strategies',
      'Add distributed tracing',
      'Optimize module communication'
    ]
  },
  
  year1: {
    focus: 'Platform maturity',
    actions: [
      'Full service mesh',
      'Self-service platform',
      'Automated testing and deployment',
      'Complete observability'
    ]
  }
};

Final Wisdom: Start with modules, not microservices. Let your architecture evolve with your needs, not ahead of them.

Build modular. Scale gradually. Maintain velocity.

The best architecture is the one that lets you change your mind.