Initializing
Back to Retrospectives

Engineering Modular Monoliths: A Domain-Driven Approach to Node.js Architecture

ArchitectureDDDNode.jsExpress

Modern backend software often bounces between two extremes: the unmanageable big ball of mud monolith, and the overly complex, high-latency network of microservices.

However, there is a powerful middle ground that balances simplicity with long-term maintainability: the Modular Monolith.

---

1. Core Principles of Modular Monoliths

A modular monolith is a single application process built of strictly isolated vertical slices representing business domains.

Our engineering framework relies on two primary architectural compliance checks:

  1. The Deletion Test: If you delete a domain directory (e.g. src/domains/suggestions/), the rest of the application must compile and boot successfully, with zero code modifications needed in other domains.
  2. The 5-File Rule: Any typical feature modification or extension should touch at most five files, all strictly localized within the same domain slice.

---

2. Dependency Direction & Strict Boundaries

The flow of dependencies is unidirectional and sacred:

code
Infrastructure (Prisma, Nginx) → Application (Express Routers, Controllers) → Domain (Services, Policies) → (Pure Business Logic)

To maintain these strict boundaries, we enforce two structural patterns:

A. The Public Domain API (`index.ts`)

Each domain slice exposes exactly one public interface file (index.ts). Other domains are strictly forbidden from importing internal directories.

typescript
// ✅ Good: Import from public API boundary
import { SuggestionsService } from '@domains/suggestions';

// ❌ Forbidden: Tunnelling into domain internals
import { SuggestionsRepository } from '@domains/suggestions/suggestions.repository';

B. Manual Dependency Injection (DI)

Rather than relying on magic decorators or complex frameworks that hide dependencies, we write a explicit manual DI container:

typescript
// src/container.ts
const db = prismaClient;
const suggestionsRepo = new SuggestionsRepository(db);
const suggestionsPolicy = new SuggestionsPolicy();

export const container = {
  suggestionsController: new SuggestionsController(
    new SuggestionsService(suggestionsRepo, suggestionsPolicy)
  )
};

This ensures our entire dependency tree is visible, fully mockable in unit tests, and compiles with complete safety.

Applied in Projects

Explore the real-world production systems and custom codebases where these engineering patterns are fully implemented.