Loading learning content...
Before we can discuss microservices, before we explore distributed systems patterns, before we dive into the complexities of service meshes and event-driven architectures, we must first understand the architectural foundation upon which all these alternatives were built: the monolith.
The monolithic architecture isn't a historical artifact or a pattern to be avoided. It is the starting point for virtually every successful software system ever built. Companies like Amazon, Netflix, eBay, and Twitter all began as monoliths. Google's initial search engine was a monolith. Facebook was a monolith. Understanding the monolith is not about learning legacy patterns—it's about understanding the baseline from which all architectural evolution begins.
By the end of this page, you will have a precise, rigorous understanding of what constitutes a monolithic architecture. You'll be able to identify monoliths in the wild, understand their internal structure, and recognize the defining characteristics that distinguish them from other architectural patterns. This foundation is essential for evaluating when monoliths remain the right choice and when evolution becomes necessary.
The term "monolith" derives from the Greek words monos (single) and lithos (stone)—a single, unified block. In software architecture, a monolithic application is one in which all functionality is deployed as a single, unified unit.
But this definition, while accurate, doesn't capture the essence of what makes a monolith a monolith. Let's be more precise:
A monolithic architecture is a software design pattern where the entire application—including user interface logic, business logic, data access logic, and background processing—is designed, developed, deployed, and scaled as a single, indivisible unit within a single process space.
This definition contains several critical implications:
Single Deployment Unit: The entire application is packaged and deployed together. You cannot deploy the payment processing module independently of the user authentication module. When you deploy, you deploy everything.
Single Process Space: All components execute within the same process (or a small number of processes on the same machine). Components communicate through in-process function calls, not network requests.
Shared Memory and State: Components can directly access shared resources—the same database connection pool, the same cache, the same configuration objects.
Unified Codebase: Typically, though not necessarily, monoliths are built from a single codebase. All developers work in the same repository, and code changes flow through a single integration and deployment pipeline.
| Characteristic | Monolith Behavior | Implication |
|---|---|---|
| Deployment | All-or-nothing: entire app deploys at once | Simple deployment, but changes require full redeploy |
| Scaling | Entire application scales together | Inefficient if only one module needs more resources |
| Communication | In-process function calls | Low latency, no network overhead, no serialization |
| Data Access | Direct database access from any module | Simple queries, but creates tight coupling |
| Failure Boundary | Single process failure affects entire app | Any critical bug can bring down the whole system |
| Technology Stack | Single language/framework typically | Consistency, but limits using best tool for each job |
To truly understand monolithic architecture, we must examine its internal structure. A well-architected monolith is not a tangled mess of code—it has layers, boundaries, and organization. The problem isn't that monoliths lack structure; it's that their structure exists only within a single deployment unit.
The Classical Layered Architecture
Most monoliths follow a layered architecture pattern, typically consisting of three or four primary layers:
The Dependency Rule
In a well-structured monolith, dependencies flow inward. The presentation layer depends on the application layer. The application layer depends on the domain layer. The domain layer should depend on nothing but its own abstractions. The infrastructure layer implements interfaces defined by the domain.
This is the essence of the Clean Architecture or Hexagonal Architecture applied within a monolith. The critical insight is that architectural discipline is not unique to microservices—monoliths can (and should) be well-architected.
Not all monoliths are well-structured. Many devolve into what architects call a Big Ball of Mud—a system with no discernible structure, where any module can call any other module, where business logic is scattered across layers, and where a change in one place causes unexpected failures elsewhere. This is not inherent to monoliths; it's a failure of discipline. Microservices do not solve this problem—they simply make the mess distributed.
Abstract definitions only take us so far. Let's examine what a monolith looks like in practice with concrete examples across different technology stacks.
Example: A Typical E-Commerce Monolith
Consider an e-commerce platform with the following capabilities: user registration and authentication, product catalog management, shopping cart functionality, order processing, payment processing, inventory management, shipping integration, and reporting.
In a monolithic architecture, all these capabilities exist within a single deployable unit:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
ecommerce-monolith/├── src/│ ├── controllers/ # Presentation Layer│ │ ├── AuthController.ts│ │ ├── ProductController.ts│ │ ├── CartController.ts│ │ ├── OrderController.ts│ │ ├── PaymentController.ts│ │ └── AdminController.ts│ ││ ├── services/ # Application Layer│ │ ├── AuthService.ts│ │ ├── ProductService.ts│ │ ├── CartService.ts│ │ ├── OrderService.ts│ │ ├── PaymentService.ts│ │ ├── InventoryService.ts│ │ ├── ShippingService.ts│ │ └── ReportingService.ts│ ││ ├── domain/ # Domain Layer│ │ ├── entities/│ │ │ ├── User.ts│ │ │ ├── Product.ts│ │ │ ├── Order.ts│ │ │ ├── Payment.ts│ │ │ └── Shipment.ts│ │ ├── value-objects/│ │ │ ├── Money.ts│ │ │ ├── Address.ts│ │ │ └── OrderStatus.ts│ │ └── services/│ │ ├── PricingDomainService.ts│ │ └── InventoryDomainService.ts│ ││ ├── infrastructure/ # Infrastructure Layer│ │ ├── repositories/│ │ │ ├── UserRepository.ts│ │ │ ├── ProductRepository.ts│ │ │ ├── OrderRepository.ts│ │ │ └── PaymentRepository.ts│ │ ├── external/│ │ │ ├── StripePaymentGateway.ts│ │ │ ├── ShippoShippingProvider.ts│ │ │ └── SendGridEmailClient.ts│ │ └── cache/│ │ └── RedisCache.ts│ ││ ├── config/ # Configuration│ │ ├── database.ts│ │ ├── cache.ts│ │ └── app.ts│ ││ └── app.ts # Application Entry Point│├── tests/├── package.json├── Dockerfile # Single container deployment└── docker-compose.ymlKey Observations
Notice that despite having distinct modules (authentication, products, orders, payments), they all:
package.json and dependency treeWhen the OrderService needs to check inventory, it doesn't make an HTTP call to an inventory service. It imports and invokes InventoryService.checkAvailability() directly—an in-process function call with nanosecond latency.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// Application Layer: OrderService// This service coordinates the order placement workflow import { InventoryService } from './InventoryService';import { PaymentService } from './PaymentService';import { ShippingService } from './ShippingService';import { OrderRepository } from '../infrastructure/repositories/OrderRepository';import { Order, OrderStatus } from '../domain/entities/Order';import { TransactionManager } from '../infrastructure/database/TransactionManager'; export class OrderService { constructor( private inventoryService: InventoryService, private paymentService: PaymentService, private shippingService: ShippingService, private orderRepository: OrderRepository, private transactionManager: TransactionManager ) {} /** * Places a new order - the entire workflow executes in a single * database transaction within the same process. */ async placeOrder(userId: string, items: OrderItem[], paymentDetails: PaymentDetails): Promise<Order> { // All operations occur within a single ACID transaction return this.transactionManager.executeInTransaction(async (trx) => { // Step 1: Reserve inventory (direct function call, no network) const reservationResult = await this.inventoryService.reserveItems(items, trx); if (!reservationResult.success) { throw new InsufficientInventoryError(reservationResult.unavailableItems); } // Step 2: Process payment (still within transaction) const paymentResult = await this.paymentService.processPayment( paymentDetails, this.calculateTotal(items), trx ); if (!paymentResult.success) { // Transaction will rollback, releasing inventory reservation throw new PaymentFailedError(paymentResult.error); } // Step 3: Create order record const order = new Order({ userId, items, paymentId: paymentResult.paymentId, status: OrderStatus.CONFIRMED, createdAt: new Date() }); await this.orderRepository.save(order, trx); // Step 4: Schedule shipping (could be async, but often in same transaction) await this.shippingService.scheduleShipment(order, trx); // If we reach here, entire transaction commits atomically return order; }); }}Notice how the entire order placement workflow—inventory reservation, payment processing, order creation, and shipping scheduling—can execute within a single database transaction. If payment fails, inventory reservation automatically rolls back. This transactional safety is trivial in a monolith and extremely complex in distributed systems.
Not all monoliths are created equal. Understanding the different types of monolithic architectures helps us appreciate the spectrum between a well-architected monolith and the chaos that causes teams to flee toward microservices.
The Modular Monolith
Between the extremes lies the modular monolith—a monolithic deployment with internal module boundaries so well-defined that modules could, in theory, be extracted into separate services. Each module:
The modular monolith represents a sweet spot: the simplicity of monolithic deployment with the architectural discipline that makes future decomposition possible.
| Characteristic | Big Ball of Mud | Traditional Monolith | Modular Monolith |
|---|---|---|---|
| Internal Structure | None / chaos | Layers, some boundaries | Strong module boundaries |
| Code Organization | Arbitrary | By layer (controllers, services) | By domain (orders, users) |
| Database Access | Anywhere to anywhere | Via repositories, but shared | Each module owns its tables |
| Testing Difficulty | Extremely hard | Moderate | Easy—modules test in isolation |
| Change Impact | Unknown, cascading | Predictable within layers | Contained within modules |
| Future Flexibility | Rewrite required | Difficult extraction | Extraction-ready |
| Recommended? | Never | Acceptable for small apps | Optimal for most cases |
To fully understand what a monolith is, we must understand what it is not. The defining contrast is with distributed systems—architectures where components run in separate processes, often on separate machines, communicating over a network.
The Network Boundary is Everything
The fundamental difference between a monolith and a distributed system is the presence of network communication between components. This single difference has profound implications:
| Aspect | Monolith | Distributed System |
|---|---|---|
| Communication | In-process function calls (nanoseconds) | Network requests (milliseconds to seconds) |
| Failure Modes | All-or-nothing process failure | Partial failures, network partitions, timeouts |
| Data Consistency | ACID transactions easy | Eventual consistency, distributed transactions complex |
| Latency | Negligible internal latency | Cumulative latency across service calls |
| Debugging | Single process, full stack trace | Distributed tracing across services |
| Deployment | One artifact, one deploy | Multiple artifacts, complex coordination |
| Operational Complexity | Lower (one thing to monitor) | Higher (many things to monitor, orchestrate) |
When you move from monolith to distributed systems, you face the classic Fallacies of Distributed Computing: The network is NOT reliable. Latency is NOT zero. Bandwidth is NOT infinite. The network is NOT secure. Topology does NOT remain constant. There is NOT one administrator. Transport cost is NOT zero. The network is NOT homogeneous. A monolith sidesteps all of these problems by keeping everything in-process.
1234567891011121314151617181920
// Monolith: Placing an order// All happens in one process, one transaction async function placeOrder(userId: string, items: CartItem[]) { const trx = await db.startTransaction(); try { // Direct function calls - nanoseconds, never fail due to "network" const user = await userService.getUser(userId, trx); const inventory = await inventoryService.reserve(items, trx); const payment = await paymentService.charge(user, total, trx); const order = await orderService.create(user, items, payment, trx); await trx.commit(); // All succeeds or all fails return order; } catch (error) { await trx.rollback(); // Clean rollback, guaranteed consistency throw error; }}The Simplicity Advantage
The monolith's simplicity is not a weakness—it's a feature. Every additional network boundary introduces:
This doesn't mean monoliths are always better. It means that distributed systems should be chosen deliberately, when the benefits outweigh these costs.
The industry discourse around monoliths has created several persistent misconceptions. Let's address them directly:
The question isn't "monolith or microservices?" The question is: "What problems do we have, and which architecture best addresses them?" For many teams—especially early-stage startups, small-to-medium applications, and teams without significant operational expertise—the answer is still the monolith.
Understanding when monoliths become problematic is as important as understanding their benefits. Problems typically emerge along several dimensions:
Many of these symptoms arise not from the monolith architecture itself, but from poor internal structure. A modular monolith with well-defined boundaries experiences fewer of these problems. Before jumping to microservices, consider whether restructuring the monolith could address the symptoms more effectively.
We've established a rigorous understanding of what constitutes a monolithic architecture. Let's consolidate the key concepts:
You now have a precise understanding of what monolithic architecture means. In the next page, we'll explore the substantial benefits that monoliths provide—the reasons why they remain the right choice for most applications and why even companies that eventually moved to microservices started as monoliths.