Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Monolithic Architecture

Sometimes the simplest approach is the best approach

A monolithic architecture is an architectural pattern where an entire application is built, deployed, and run as a single unit. All features, components, and modules exist in one codebase, compiled into one executable, and deployed as one process. Think of it like building a castle from one massive stone block—everything is connected and inseparable.

Diagram

The term “monolithic” comes from Greek: mono (one) + lithos (stone). Like ancient monuments carved from a single massive stone, a monolithic application is one cohesive unit. This doesn’t mean it’s poorly structured—a well-designed monolith can be modular, maintainable, and scalable.


Real-World Scenario: Stack Overflow’s Monolith

Section titled “Real-World Scenario: Stack Overflow’s Monolith”

Stack Overflow serves 1.3 billion page views per month with just 9 web servers and 4 SQL servers running a single monolithic .NET application. This demonstrates that monoliths can handle massive scale when designed correctly.

Why Stack Overflow chose a monolith:

  • Small team that knows the codebase intimately
  • Focus on performance over distribution
  • Simplicity in deployment and operations
  • Strong consistency requirements for Q&A data

Key lesson: Architecture choice depends on your constraints. Stack Overflow optimized for their constraints: small team, monolithic codebase, and performance focus. Their architecture is optimized around these constraints, not against them.


A typical monolithic application contains all layers and components in a single codebase:

Diagram

All components—user interface, business logic, data access, and database—exist in one codebase. When you deploy, you deploy everything together as a single unit. This simplicity is both the monolith’s greatest strength and its greatest weakness.


Not all monoliths are created equal. Understanding the different types helps you build better monoliths and recognize when you need to evolve.

This is what gives monoliths a bad name. Code is tangled, modules depend on everything, and changes in one place break things everywhere. There are no clear boundaries, god classes with thousands of lines, direct database access from UI code, and copy-paste programming.

Diagram

A well-structured monolith with clear module boundaries, clean interfaces, and strong separation of concerns. Each module has a single responsibility, modules communicate through well-defined interfaces, and each module could theoretically become a microservice if needed.

Diagram

Characteristics:

  • Clear module boundaries (e.g., Orders, Payments, Users)
  • Modules communicate through well-defined interfaces
  • Each module could theoretically become a microservice
  • Strong encapsulation within modules

Organized into horizontal layers with clear responsibilities. Each layer has a specific purpose, and dependencies flow in one direction—from presentation to application to domain to infrastructure.

Diagram

Monoliths offer incredible simplicity for developers. You have one codebase, one IDE project, one repository. You can see the entire application flow from HTTP request to database query in one place. This makes onboarding new developers easier and allows for atomic changes across multiple modules.

For beginners: Everything is in one place. You can trace a request from the controller through the service layer to the database and back. No need to understand multiple services, network calls, or distributed systems concepts.

For senior engineers: Refactoring is straightforward with IDE support for finding all usages and automated refactoring tools. You can make changes across multiple modules atomically, ensuring consistency.

Debugging a monolith is straightforward. You can set breakpoints anywhere in the flow, run the entire application locally, write integration tests that exercise the full stack, and use profilers to find performance bottlenecks end-to-end.

Debugging: Set breakpoints in controllers, services, repositories—anywhere in the codebase. Step through the entire request flow in one debugging session. No need to debug across multiple services or network boundaries.

Testing: Write integration tests that exercise the full stack. Test database transactions, business logic, and API endpoints all in one test. This gives you confidence that the entire system works together.

Deployment is straightforward—build once, deploy once. Your entire application is packaged as a single artifact (JAR, WAR, container image) and deployed as one unit.

Diagram

The process: Build your application into a single artifact. Deploy that artifact to your server or container. Start one process. Your entire application is running. No need to coordinate multiple deployments, manage service dependencies, or handle partial deployments.

Monoliths have significant performance advantages due to in-process communication. Function calls are microseconds instead of milliseconds for network calls. There’s no serialization/deserialization overhead, shared memory between components, and no network latency.

Performance comparison:

OperationMonolithMicroservices
Function call0.001 ms-
In-process message0.01 ms-
HTTP REST call-10-50 ms
Message queue-5-20 ms

Real-world impact: A monolith can process thousands of requests per second with in-process calls. The same operations in microservices require network calls, adding 10-50ms per call. This latency compounds across multiple service calls, significantly impacting performance.

Monoliths provide strong transactional consistency through ACID guarantees. You can perform multiple operations in a single database transaction, ensuring all-or-nothing semantics.

Real-world example: Transferring money between accounts. In a monolith, this is one database transaction—either both accounts update or neither does. In microservices, this requires distributed transactions or eventual consistency patterns, which are more complex and less reliable.


When you deploy a monolith, you deploy everything. A bug in a minor feature can take down your entire application. This all-or-nothing deployment model increases risk—one bad deployment affects all functionality.

Diagram

You must scale the entire application even if only one component needs more resources. If the Orders module is busy but Reports is idle, you still scale both together.

Diagram

The problem: You can’t scale individual components independently. If Orders needs 10 instances but Reports only needs 1, you still deploy 10 instances of everything, wasting resources on idle components.

Once you choose a technology stack, you’re committed. Started with Python? The entire app must be Python. Chose PostgreSQL? Everything uses PostgreSQL. Want to try Rust for CPU-intensive tasks? Too bad—you’re locked into your initial choices.

Real-world impact: You might have a specific component that would benefit from a different technology (e.g., Go for high-concurrency, Rust for performance-critical code), but you can’t use it in a monolith without rewriting the entire application.

As the codebase grows, build and startup times increase significantly. Build time grows from 2 minutes to 10 minutes to 30 minutes. Test suites take longer—5 minutes becomes 30 minutes becomes 2 hours. Startup time increases from 5 seconds to 30 seconds to 2 minutes.

Impact: This slows down development velocity. Developers wait longer for builds and tests. Local development becomes slower. CI/CD pipelines take longer. The feedback loop between making changes and seeing results lengthens.

With many engineers working on one codebase, coordination becomes difficult. Merge conflicts become frequent as multiple developers work on the same files. Code reviews take longer because reviewers must understand the entire codebase. Release coordination is complex—everyone must coordinate their changes. “Who owns this code?” becomes unclear.

Real-world example: With 50 engineers working on one monolith, you might have 10 pull requests waiting for review, each touching multiple files. Merge conflicts occur daily. Releases require coordination across all teams. This overhead slows down development significantly.


LLD Connection: Designing a Modular Monolith

Section titled “LLD Connection: Designing a Modular Monolith”

Designing a modular monolith requires applying SOLID principles at the module level. The same principles that make classes clean also make monoliths maintainable.

Single Responsibility Principle (Module Level)

Section titled “Single Responsibility Principle (Module Level)”

Each module should have one reason to change. The Orders module handles orders, the Payments module handles payments, and the Users module handles users. This separation makes the codebase easier to understand, test, and maintain.

Real-world example: An e-commerce application has three modules: Orders, Payments, and Users. The Orders module is responsible only for order management—creating orders, updating orders, canceling orders. It doesn’t handle payment processing or user management directly. Instead, it uses interfaces to communicate with other modules.

Dependency Inversion Principle (Module Level)

Section titled “Dependency Inversion Principle (Module Level)”

Modules should depend on abstractions, not concrete implementations. The Orders module depends on the PaymentClient interface, not the PaymentService implementation. This allows modules to be decoupled and makes the system more flexible.

Diagram

Benefits: The Orders module doesn’t know about the Payments module implementation. You can change the PaymentService implementation without affecting Orders. You can even replace it with a mock for testing. This decoupling makes the system more maintainable and testable.


Starting a new project: You don’t know what will succeed yet. Premature microservices equal premature optimization. Martin Fowler’s advice: “Start with a monolith, extract microservices later.” Build a monolith first, learn what works, then extract services when you have real data about bottlenecks.

Small to medium-sized teams: Team size of 1-20 developers works well with monoliths. Everyone can understand the entire codebase. There’s less operational complexity to manage. Communication overhead is manageable.

Predictable load patterns: All features have similar traffic patterns. There’s no need for independent scaling. If everything scales together, a monolith is simpler than microservices.

Strong consistency requirements: Financial transactions, inventory management, and booking systems require strong consistency. Monoliths provide ACID guarantees through single-database transactions, which are simpler than distributed transactions.

Rapid development phase: MVP development, startup mode, and experimenting with features benefit from monolith simplicity. You can move fast, iterate quickly, and change direction easily.

Large teams (50+ engineers): Coordination overhead becomes too high. Multiple teams step on each other’s toes. Merge conflicts become daily occurrences. Consider microservices to allow teams to work independently.

Different scaling needs: Search service needs 100 instances, but admin panel needs 2 instances. If components have vastly different scaling requirements, microservices allow independent scaling.

Technology diversity required: ML models in Python, real-time processing in Go, legacy integration in Java. If you need different technologies for different problems, microservices allow polyglot architectures.


Stack Overflow: The Monolith that Serves Billions

Section titled “Stack Overflow: The Monolith that Serves Billions”

Stack Overflow serves 1.3 billion page views per month with 9 web servers and 4 SQL servers running one monolithic .NET application. This demonstrates that monoliths can handle massive scale.

Why it works:

  • Highly optimized code
  • Efficient caching strategy
  • Excellent database design
  • Team knows the codebase intimately

Shopify powers millions of stores and processes billions in sales while remaining largely monolithic (with some extracted services). Their approach includes strong module boundaries enforced by tooling, “componentization” within the monolith, and extracting to services only when absolutely necessary.

Key insight: You don’t need to extract everything to microservices. A well-designed modular monolith can scale to massive size. Extract services only when you have a clear need—different scaling requirements, technology needs, or team boundaries.

David Heinemeier Hansson (creator of Ruby on Rails) advocates for the “Majestic Monolith”—a monolith that’s easier to understand, easier to develop, easier to deploy, and performs better than microservices.

Their philosophy: “The Majestic Monolith can become The Citadel. A monolith that deploys one thousand knights of logic in a single unit.” This challenges the assumption that microservices are always better. Sometimes, a well-designed monolith is the right choice.


Use tooling to prevent unwanted dependencies. Architecture tests can verify that modules only depend on interfaces, not implementations. This prevents the monolith from becoming a big ball of mud.

Real-world example: An architecture test verifies that the Orders module doesn’t import from payments.service or payments.repository. It can only use the PaymentClient interface. This enforces boundaries and prevents tight coupling.

Instead of direct method calls between modules, use domain events. Modules publish events when something happens, and other modules subscribe to those events. This decouples modules and makes the system more flexible.

Benefits: Modules don’t need to know about each other. The Orders module publishes OrderCreatedEvent without knowing who listens. The Payments module subscribes to OrderCreatedEvent without knowing who publishes it. This loose coupling makes the system easier to change.

The “Shared Kernel” should only contain common value objects (Money, Address, Email), base exceptions, utility functions, and event definitions. It should not contain business logic or domain entities specific to one module.

Anti-pattern: Putting business logic in the shared kernel creates tight coupling. All modules depend on shared business logic, making changes difficult. Keep shared code minimal and focused on truly common concerns.

Option A: Shared Database with Module-Specific Schemas

Each module has its own schema. The Orders module uses the orders schema, Payments uses the payments schema. This provides logical separation while using one physical database.

Option B: Shared Database with Access Rules

Modules access only their own tables. The OrderRepository can access the orders table directly, but PaymentService must use the OrderClient interface to access order data. This enforces boundaries at the database level.


AspectMonolithMicroservices
ComplexityLowHigh
Development SpeedFast (initially)Slow (initially)
DeploymentSimple, riskyComplex, safer
ScalingVertical, entire appHorizontal, per service
ConsistencyStrong (ACID)Eventual
PerformanceExcellent (in-process)Good (network overhead)
Team Size1-20 optimal20+ optimal
TechnologyOne stackPolyglot
DebuggingEasyDifficult
TestingStraightforwardComplex
Operational CostLowHigh

Key insight: Neither is universally better. Choose based on your constraints: team size, scale requirements, consistency needs, and operational capabilities.


Migration Path: When to Break Up the Monolith

Section titled “Migration Path: When to Break Up the Monolith”

Deployment becomes risky: Fearful of releasing, frequent rollbacks, long testing cycles. If deploying feels dangerous, consider extracting services to reduce deployment risk.

Scaling inefficiency: 90% of resources used by 10% of features. Can’t scale just what you need. If scaling the entire application wastes resources, consider microservices.

Team bottlenecks: Teams waiting on each other, merge conflicts daily, unclear ownership. If coordination overhead is too high, microservices allow teams to work independently.

Technology constraints: Need different languages for different problems, performance issues in specific areas. If you need technology diversity, microservices allow polyglot architectures.

The Gradual Approach (Strangler Fig Pattern)

Section titled “The Gradual Approach (Strangler Fig Pattern)”

Don’t rewrite! Extract services gradually using the Strangler Fig Pattern:

  1. Identify boundaries - Which module is most independent?
  2. Add interfaces - Define clean API contracts
  3. Duplicate functionality - New service implements same interface
  4. Route gradually - Route 5% → 20% → 100% to new service
  5. Remove old code - Once migration is complete

This approach minimizes risk and allows gradual migration without big-bang rewrites.


Start Simple

Begin with a monolith. You can always extract services later when you have real data about bottlenecks.

Modular is Key

A well-designed modular monolith with clear boundaries is better than poorly designed microservices.

Know Your Constraints

Choose architecture based on your team size, scale requirements, and organizational structure.

Performance Matters

In-process communication is 100-1000x faster than network calls. Don’t sacrifice performance without reason.



  • “Monolith to Microservices” by Sam Newman
  • “The Majestic Monolith” by David Heinemeier Hansson
  • “Modular Monoliths” by Simon Brown
  • Stack Overflow Architecture - Blog posts by Nick Craver