Modular Monolith

Production engineering guide for modular monolith covering patterns, implementation strategies, and operational best practices.

A modular monolith is a software architecture approach where a single application is organized into distinct functional modules that maintain clear boundaries and dependencies, but remain deployed as one unit. This pattern is appropriate when you need bounded contexts within a single service while avoiding the complexity of distributed systems.

Define Module Boundaries

Use a consistent naming convention for modules based on business domains. For example, in a system with user management and order processing, create directories like user-service/ and order-processing/.

Structure your codebase with clear module boundaries:

src/
├── user-service/
│   ├── models/
│   ├── controllers/
│   ├── services/
│   └── repository/
├── order-processing/
│   ├── models/
│   ├── controllers/
│   ├── services/
│   └── repository/
└── shared/
    ├── utils/
    └── exceptions/

Avoid circular dependencies between modules by using dependency inversion principles. When one module needs to access another, define interfaces in the shared layer or use dependency injection patterns.

Implement Module Communication

Modules communicate through well-defined APIs using either direct function calls or message passing. For internal communication, prefer synchronous function calls within the same process:

# user_service.py
from order_processing import get_orders_by_user

def get_user_profile(user_id):
    user = fetch_user(user_id)
    orders = get_orders_by_user(user_id)
    return {**user, 'orders': orders}

For inter-module communication, use a message broker like RabbitMQ or Kafka:

# config.yaml
message_broker:
  host: localhost
  port: 5672
  exchanges:
    - name: user_events
      type: topic

Enforce Module Isolation

Create module-specific configuration files that define their dependencies and behavior:

# user-service/config.yaml
database:
  url: postgres://localhost:5432/user_db
  pool_size: 10
logging:
  level: INFO
  format: json

Configure Module Dependencies

Use a dependency management system like pip or npm to track module dependencies. For Python, define requirements in separate files:

# user-service/requirements.txt
sqlalchemy==1.4.23
pydantic==1.8.2

Implement Module-Specific Error Handling

Each module should have its own error types and handling:

class UserNotFoundError(Exception):
    def __init__(self, user_id):
        self.user_id = user_id
        super().__init__(f"User {user_id} not found")

def get_user(user_id):
    try:
        return database.get_user(user_id)
    except DatabaseError:
        raise UserNotFoundError(user_id)

Manage Module Lifecycle

Use a deployment tool that supports module-level deployments. For example, with Docker Compose:

# docker-compose.yml
version: '3.8'
services:
  user-service:
    build: ./user-service
    ports:
      - "8001:8000"
  order-processing:
    build: ./order-processing
    ports:
      - "8002:8000"

Monitor Module Performance

Each module should expose its own metrics endpoint:

# In each module's HTTP handler
@app.route('/metrics')
def get_metrics():
    return jsonify({
        'request_count': request_counter,
        'error_count': error_counter,
        'response_time': response_timer
    })

Handle Cross-Module Transactions

When operations span multiple modules, implement distributed transaction patterns:

# Example using saga pattern
def process_order(user_id, items):
    order_id = create_order_in_order_processing(user_id, items)
    
    try:
        # Update inventory in inventory module
        update_inventory(items)
        # Charge user in payment module
        charge_user(user_id, calculate_total(items))
        
        # Confirm order in order processing
        confirm_order(order_id)
    except Exception as e:
        # Rollback in reverse order
        rollback_payment(user_id, calculate_total(items))
        rollback_inventory(items)
        raise

Configure Module-Specific Logging

Set up structured logging for each module with consistent log formats:

import logging

logger = logging.getLogger('user_service')
logger.setLevel(logging.INFO)

handler = logging.StreamHandler()
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)

Implement Module-Level Testing

Each module should have its own test suite:

# user_service/test_user_service.py
import unittest
from user_service import get_user

class TestUserService(unittest.TestCase):
    def test_get_user_exists(self):
        user = get_user(123)
        self.assertEqual(user['id'], 123)

if __name__ == '__main__':
    unittest.main()

Define Module Boundaries in Code

Use code organization patterns to enforce boundaries:

# user_service/__init__.py
from .controllers import user_router
from .services import UserService

# Export only the public API
__all__ = ['user_router', 'UserService']

Configure Database Per Module

Each module should have its own database schema or schema namespace:

-- user-service/schema.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

Handle Module-Specific Configuration

Use environment variables or configuration files that are scoped to each module:

# Environment variables for user service
USER_SERVICE_DB_URL=postgresql://localhost:5432/user_db
USER_SERVICE_LOG_LEVEL=INFO

Enforce Module Boundaries at Build Time

Use build tools to prevent circular imports:

# Makefile
check-modules:
	pip-check-reqs user-service/
	pip-check-reqs order-processing/

Manage Module Deployment

Each module should have its own deployment configuration:

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 8000

Implement Module-Specific Health Checks

Each module should expose health check endpoints:

@app.route('/health')
def health_check():
    # Check database connectivity
    db_status = check_database()
    # Check dependencies
    dep_status = check_dependencies()
    
    if db_status and dep_status:
        return jsonify({'status': 'healthy'})
    else:
        return jsonify({'status': 'unhealthy'}), 503

Handle Module-Specific Secrets

Each module should manage its own secrets:

# config/secrets.yaml
user-service:
  database_password: "secret_password"
  api_key: "api_key_value"

Configure Module-Level Caching

Implement caching strategies specific to each module:

from functools import lru_cache

@lru_cache(maxsize=128)
def get_user_cached(user_id):
    return fetch_user(user_id)

Monitor Module-Specific Metrics

Each module should track its own metrics:

from prometheus_client import Counter, Histogram

request_count = Counter('user_service_requests_total', 'Total requests')
response_time = Histogram('user_service_request_duration_seconds', 'Request duration')

Implement Module-Specific Rate Limiting

Each module should enforce its own rate limits:

from flask_limiter import Limiter

limiter = Limiter(
    key_func=lambda: request.remote_addr,
    default_limits=["100 per hour"]
)

Handle Module-Specific Authentication

Each module should manage its own authentication logic:

def authenticate_user(token):
    # Validate token against module-specific requirements
    if not verify_token(token):
        raise AuthenticationError("Invalid token")
    return get_user_from_token(token)

Enforce Module-Level Testing Coverage

Configure code coverage for individual modules:

# .coveragerc
[run]
source = user-service/
omit = 
    */tests/*
    */migrations/*

Configure Module-Specific CI/CD Pipelines

Each module should have independent CI/CD workflows:

# .github/workflows/user-service.yml
name: User Service CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run tests
      run: python -m pytest user-service/

Manage Module-Specific Dependencies

Use package managers to track dependencies for each module:

{
  "name": "user-service",
  "dependencies": {
    "express": "^4.18.0",
    "sequelize": "^6.20.0"
  }
}

Implement Module-Level Versioning

Each module should manage its own versioning strategy:

# package.json
{
  "name": "user-service",
  "version": "1.2.3",
  "description": "User management service"
}

Configure Module-Specific Monitoring

Set up monitoring tools specific to each module:

# prometheus.yml
scrape_configs:
  - job_name: 'user-service'
    static_configs:
      - targets: ['localhost:8001']

Handle Module-Specific Rollbacks

Each module should support independent rollbacks:

# Rollback user service to previous version
docker run --rollback user-service:v1.2.1

Enforce Module-Level Documentation

Each module should have its own documentation:

# User Service

This module handles user-related operations including registration, authentication, and profile management.

## API Endpoints

- GET /users/{id}
- POST /users

Configure Module-Specific Security

Each module should implement its own security measures:

from flask import request
from functools import wraps

def require_auth(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not authenticate_user(token):
            return {'error': 'Unauthorized'}, 401
        return f(*args, **kwargs)
    return decorated_function

Manage Module-Level Data Migrations

Each module should handle its own database migrations:

# Run migrations for user service
python -m alembic -c user-service/alembic.ini upgrade head

Implement Module-Specific Backup Strategies

Each module should define its own backup procedures:

# Backup user database
pg_dump -h localhost -U user_db_user user_db > backup_user_service.sql

Handle Module-Specific Disaster Recovery

Each module should have independent recovery plans:

# disaster-recovery.yaml
user-service:
  backup_location: s3://backups/user-service/
  recovery_time_objective: 1 hour
  recovery_point_objective: 15 minutes

The sharp edges of modular monoliths include silent dependency violations that only surface under load, incorrect configuration scoping that breaks module isolation, and the tendency to treat modules as separate services without proper boundary enforcement. Module boundaries become fragile when not strictly enforced through code organization, CI checks, and deployment practices.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.