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:
- 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. - 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:
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.
// ✅ 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:
// 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.