Modular Monoliths vs. Microservices: The Pragmatic Architecture of MSC Company
Why MSC Company chose the modular monolith in Bun monorepos over microservices: no network overhead and single ACID transactions.
The Allure and Disaster of Premature Microservices
Between 2018 and 2023, the global software industry was swept by the dogma that any serious software company needed to adopt microservices from day one. Inspired by tech giants operating with thousands of engineers, mid-sized teams aggressively split unified codebases into dozens of isolated repositories and containers.
In the vast majority of cases, this resulted in the dreaded Distributed Monolith:
- Compounded Network Latency: A user action that previously executed in memory in under 2ms suddenly triggered 8 internal HTTP/gRPC hops, elevating p99 latency beyond 350ms.
- The Death of ACID Transactions: Atomic business operations (e.g., deducting an account balance and reserving inventory) required complex distributed sagas, leading to synchronization bugs and orphan database states.
- Crippling DevOps Overhead: Teams spent more time managing Kubernetes clusters, service meshes, and fragmented CI/CD pipelines than delivering actual customer value.
At MSC Company, guided by our engineering constitution, we follow a pragmatic principle: Modular Monoliths by Domain.
What is a Modular Monolith?
A Modular Monolith combines the deployment simplicity and operational clarity of a monolith with the strict domain encapsulation of microservices.
The entire platform lives within a single, unified Bun monorepo, deploys as a cohesive Docker container on dedicated VPS infrastructure, and enforces rigid internal boundaries via Domain-Driven Design (DDD):
+-----------------------------------------------------------------------------------+
| MSC MODULAR MONOLITH ARCHITECTURE |
| |
| [ Ingress Gateway (Elysia on Bun) ] |
| | |
| v (In-Memory Function Calls - 0ms Network Latency) |
| +-----------------------------------------------------------------------------+ |
| | UNIFIED MONOREPO CONTAINER | |
| | | |
| | +-----------------------+ +------------------------+ +----------------+ | |
| | | Domain: Sales & Chat | | Domain: Billing & Sub | | Domain: AI Core| | |
| | | - WhatsApp Webhooks | | - Stripe Checkout | | - Prometheus | | |
| | | - Lead Qualification | | - Invoicing & Tax | | - RAG / Vector | | |
| | +-----------------------+ +------------------------+ +----------------+ | |
| | \ | / | |
| | v v v | |
| | +-----------------------------------------------------------------------+ | |
| | | Unified Persistence: PostgreSQL 16 + Drizzle ORM | | |
| | | (Single ACID transactions across domains with zero sync latency) | | |
| | +-----------------------------------------------------------------------+ | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
Four Concrete Production Advantages
1. In-Memory Function Calls with Zero Network Latency
When the WhatsApp Sales Module needs to query pricing logic or check contract terms, it calls a typed TypeScript function in memory. There is no JSON serialization, no TLS handshake, and no risk of network timeout. Execution occurs in microseconds.
2. True ACID Transactions via Drizzle ORM
When an event booking requires creating a lead, locking calendar availability, and generating an invoice, everything executes inside a single PostgreSQL 16 transaction:
await db.transaction(async (tx) => {
const lead = await salesModule.createLead(tx, leadData);
await calendarModule.reserveDate(tx, lead.id, eventDate);
await billingModule.generateInvoice(tx, lead.id, totalAmount);
});
// If any step fails, the database automatically rolls back with zero corruption!
3. Instant Refactoring and Strict Type Safety
Because all domains share the same TypeScript compiler, renaming a database field or refactoring an interface immediately updates all callsites across the entire codebase at compile time.
4. Single-Command Deployments
A single CI/CD build compiles and deploys the entire stack in under 30 seconds, eliminating cross-service version mismatches.
When Should a Module be Extracted into a Separate Service?
At MSC Company, we only extract a module into an independent service when justified by strict hardware and compute constraints:
| Extraction Trigger | Architectural Decision | Real Example at MSC |
|---|---|---|
| Specialized Hardware (GPU) | 🟢 Extract to dedicated container | Whisper speech-to-text nodes running on NVIDIA GPUs |
| Asymmetrical Background CPU Spikes | 🟢 Extract to async worker | Massive document ingestion and batch PDF processing |
| "To organize the code better" | 🔴 NEVER extract | Keep encapsulated inside the Modular Monolith |
Frequently Asked Questions (FAQ AEO)
Won't a modular monolith become spaghetti code over time?
No, as long as your team enforces strict module boundary linting. In our architecture, domains only interact through public service APIs exported in index.ts, strictly prohibiting direct internal imports or raw database cross-queries.
How does a modular monolith scale under high traffic?
Horizontal scaling is straightforward: deploy multiple identical stateless Docker containers behind a Traefik v3 reverse proxy. Load balancing distributes traffic across all containers without operational friction.
Related Articles & Next Steps:
- Learn about our database layer in PostgreSQL as the Single Source of Truth.
- Discover our backend framework in Elysia vs. FastAPI on Bun Runtime.
- Explore model distillation at Cendar Lab.
- Want to simplify your enterprise software architecture? Consult with MSC Company.