00:00

Monolithic Architecture in System Design

Last updated:

A monolithic architecture builds an entire application as one unit: one codebase, one build, one deployable artifact and usually one database. The user interface, business rules and data access all live together and run in the same process. Most successful products, from small business tools to some of the busiest websites on the internet, began life as a monolith.

This tutorial explains what a monolith looks like inside, how a request flows through it, the different kinds of monoliths, how monoliths are deployed and scaled, their strengths and weaknesses, and how to build a well-structured modular monolith with Spring Boot. It ends with the warning signs that a monolith is being outgrown, common mistakes and best practices.

1. What Is Monolithic Architecture?

The word monolith means a single large block of stone. In software, a monolith is an application whose features are all built, tested and released together as a single piece.

Think of a traditional department store under one roof. Clothing, electronics, groceries and the billing counters are separate departments, but they share one building, one entrance, one electricity supply and one opening time. If the store renovates its electronics section, the whole store may have to close for the day. A monolithic application works the same way: features are organised into separate parts of the code, but they are packaged and run together.

Key characteristics:

  • Single codebase: all features live in one repository and one project.
  • Single deployable: the build produces one artifact, such as shop-app.jar, that contains everything.
  • In-process communication: parts of the application call each other through normal method calls, not over the network.
  • Shared database: usually one database holds the data for every feature.
  • Deployed and scaled as a whole: to release one change or add capacity, you redeploy or copy the entire application.

2. Inside a Monolith: Layers and Modules

A monolith is not a big ball of random code. A well-built monolith has clear internal structure, usually organised in layers and, ideally, in business modules:

The structure of a monolithic application Browsers and mobile apps send requests to a single deployable called shop-app.jar. Inside it are three layers: a presentation layer with web pages and REST controllers, a business layer with Users, Orders, Inventory and Payments modules, and a data access layer with repositories. All of it uses one database. Browsers & mobile apps shop-app.jar · ONE DEPLOYABLE Presentation layer · web pages, REST controllers Users Orders Inventory Payments Data access layer · repositories modules call each other with plain method calls One database built together tested together deployed together scaled together
Figure 1: A monolith packages the presentation, business and data access layers, and every business module, into one deployable that talks to one database.
  • Presentation layer: receives HTTP requests and returns HTML pages or JSON. In Spring Boot, these are @Controller and @RestController classes.
  • Business (service) layer: contains the rules of the business, such as "an order cannot be placed for out-of-stock items". In Spring Boot, these are @Service classes.
  • Data access layer: reads and writes the database, typically through JPA repositories.

Layers organise code by technical role. Business modules organise it by feature: users, orders, inventory and payments. The best monoliths combine both, so each feature module has its own controllers, services and repositories, and modules talk to each other only through small public interfaces.

3. How a Request Flows Through a Monolith

Suppose a customer places an order on an online shop built as a monolith:

  1. The browser sends POST /orders to the server running shop-app.jar.
  2. The OrderController validates the input and calls OrderService.placeOrder().
  3. OrderService calls InventoryService.reserve() and PaymentService.charge(). These are ordinary Java method calls inside the same process, taking microseconds.
  4. All database changes (the order row, the stock update, the payment record) happen inside one database transaction. If the payment fails, everything is rolled back automatically.
  5. The controller returns 201 Created with the new order.

This simplicity is the monolith's superpower. There are no network failures between components, no message formats to agree on, and no partial updates to clean up, because the database guarantees all-or-nothing behaviour.

4. Types of Monoliths

Not all monoliths are equal. The difference between a pleasant monolith and a painful one is internal structure:

Traditional monolith, modular monolith and distributed monolith Three panels. Traditional monolith: four modules inside one box, all connected to each other with tangled red lines. Modular monolith: four modules inside one box connected only through a few clean arrows. Distributed monolith: three separately deployed services all connected to one shared database, marked as an anti-pattern. TRADITIONAL Orders Users Stock Billing everything calls everything hard to change safely MODULAR Orders Users Stock Billing calls only through public APIs easy to change and split later DISTRIBUTED MONOLITH Orders svc Users svc Stock svc shared database separate deploys, shared data anti-pattern: worst of both
Figure 2: A traditional monolith tangles every module together. A modular monolith keeps clean boundaries inside one deployable. A distributed monolith splits deployment but keeps a shared database and tight coupling, getting the worst of both.
TypeDescriptionVerdict
Traditional (layered) monolithOrganised only by technical layers; any class can call any other; one shared data modelFine when small, becomes a "big ball of mud" as it grows
Modular monolithSplit into business modules with clear boundaries and public APIs, still deployed as one unitAn excellent default for most new products
Distributed monolithSplit into separately deployed services that still share a database or must be deployed togetherAn anti-pattern: network complexity without independence

5. A Monolithic Spring Boot Application

Here is how a small online shop might be structured as a modular monolith. Packages are organised by business feature, and each module exposes only what other modules need:

Project structure
shop-app/
├── pom.xml                          one build, one artifact: shop-app.jar
└── src/main/java/com/javabytechie/shop/
    ├── ShopApplication.java         single entry point
    ├── orders/
    │   ├── OrderController.java
    │   ├── OrderService.java        public API of the orders module
    │   └── internal/                implementation details, not used by other modules
    │       ├── Order.java
    │       └── OrderRepository.java
    ├── inventory/
    │   ├── InventoryService.java    public API of the inventory module
    │   └── internal/
    └── payments/
        ├── PaymentService.java      public API of the payments module
        └── internal/

The order module coordinates the other modules with plain method calls, and Spring wraps the whole operation in one database transaction:

OrderService.java
package com.javabytechie.shop.orders;

import java.math.BigDecimal;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.javabytechie.shop.inventory.InventoryService;
import com.javabytechie.shop.orders.internal.Order;
import com.javabytechie.shop.orders.internal.OrderRepository;
import com.javabytechie.shop.payments.PaymentService;

@Service
public class OrderService {

    private final OrderRepository orders;
    private final InventoryService inventory;
    private final PaymentService payments;

    public OrderService(OrderRepository orders, InventoryService inventory, PaymentService payments) {
        this.orders = orders;
        this.inventory = inventory;
        this.payments = payments;
    }

    /**
     * One method call, one transaction: if reserving stock or charging the card fails,
     * every change made here is rolled back together.
     */
    @Transactional
    public Order placeOrder(long customerId, String sku, int quantity, BigDecimal unitPrice) {
        Order order = orders.save(Order.create(customerId, sku, quantity, unitPrice));
        inventory.reserve(sku, quantity);                                 // in-process call, no network
        payments.charge(customerId, unitPrice.multiply(BigDecimal.valueOf(quantity)), order.getId());
        order.markConfirmed();
        return order;
    }
}

Notice what is missing compared with a microservices version: no HTTP clients, no retries, no timeouts, no message broker and no compensating actions. When the code fits in one process, the framework and the database handle consistency for you.

To keep module boundaries honest as the code grows, teams often add automated architecture tests (for example with ArchUnit) or use Spring Modulith, which can verify that modules only use each other's public APIs.

6. Building, Deploying and Scaling a Monolith

A monolith has a simple delivery pipeline: one build produces one artifact, which is tested and deployed as a whole. You can run it on a virtual machine, in a container or on a platform service.

To handle more traffic, you can scale it in two ways:

  • Vertically: give the server more CPU and memory.
  • Horizontally: run several identical copies behind a load balancer. This works well as long as the application is stateless, keeping sessions in a shared store rather than server memory.
Scaling a monolith horizontally Users connect to a load balancer that spreads requests across three identical copies of the whole application. Every copy contains all modules. All copies share one database, which can become the bottleneck. Users Load Balancer shop-app copy 1users · orders · inventory · payments shop-app copy 2users · orders · inventory · payments shop-app copy 3users · orders · inventory · payments Shared database often the first bottleneck
Figure 3: A monolith scales by running whole copies of the application behind a load balancer. Every copy contains every module, and they all share one database.

The limitation is that you always scale everything. If only the search feature is busy, you still run extra copies of the payment and admin code. In practice this matters less than people think, because idle code costs little; the real bottleneck is usually the shared database, which you can relieve with caching, read replicas and good indexing. See Scalability for details.

7. Advantages of a Monolith

  • Simple to develop: one project to open, one application to run locally, one place to search for code.
  • Simple to test: end-to-end tests run against a single application without starting a fleet of services.
  • Simple to deploy: one artifact, one pipeline, one version to track.
  • Fast internal calls: method calls take nanoseconds to microseconds, compared with milliseconds for network calls.
  • Strong consistency: one database transaction can cover any business operation.
  • Easy debugging: one log stream and one stack trace show the whole story of a request.
  • Lower operating cost: fewer servers, no service mesh, no distributed tracing infrastructure, a smaller operations team.
  • Easy refactoring: the IDE can safely rename and move code across features, because it is all one codebase.

8. Disadvantages of a Monolith

  • Growing complexity: without discipline, modules become entangled and every change risks breaking something unrelated.
  • Slow builds and startups: very large codebases take longer to compile, test and start.
  • Deployment coupling: a small fix in one feature requires redeploying the whole application, and one team's bug can block everyone's release.
  • Coarse scaling: you cannot scale one busy feature independently.
  • Shared failure: a memory leak or crash in one feature can take down the entire application.
  • Technology lock-in: the whole application shares one language, framework and version; upgrading is an all-at-once project.
  • Team scaling: when dozens of developers work in one codebase, coordination, merge conflicts and release scheduling become harder.

9. When a Monolith Is the Right Choice

A monolith, ideally a modular one, is usually the best starting point when:

  • You are building a new product and the requirements and business boundaries are still changing.
  • The team is small, from one person to a few teams that can coordinate releases easily.
  • The application has moderate scale; a monolith with caching and replicas can serve millions of requests per day.
  • You need strong consistency across features, such as in accounting or inventory systems.
  • Speed of delivery and low operational cost matter more than independent deployments.

Many experienced architects recommend "monolith first": build a modular monolith, learn where the real boundaries are, and extract services only when a specific need appears. Compare the options in Monolithic vs Microservices Architecture.

10. Designing a Good Modular Monolith

A modular monolith gives you most of the organisational benefits of microservices while keeping the simplicity of one deployable. These rules keep it healthy:

  1. Package by feature, not by layer. Put everything for orders in an orders package instead of spreading it across global controllers, services and repositories packages.
  2. Expose a small public API per module. Other modules call InventoryService, never the inventory repository or entities directly.
  3. Own your tables. Each module reads and writes only its own tables. If orders need product names, they ask the catalogue module or keep their own copy.
  4. Prefer events for side effects. When an order is placed, publish an OrderPlaced event; the email and loyalty modules react to it instead of being called directly.
  5. Enforce boundaries automatically with architecture tests in the build, so violations are caught in code review, not years later.

Spring makes in-process events easy:

OrderPlacedEvent.java and listeners
package com.javabytechie.shop.orders;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;

// Public event of the orders module: other modules depend on this, not on order internals
public record OrderPlacedEvent(long orderId, long customerId, String email) { }

@Component
class OrderEvents {

    private final ApplicationEventPublisher publisher;

    OrderEvents(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    void orderPlaced(long orderId, long customerId, String email) {
        publisher.publishEvent(new OrderPlacedEvent(orderId, customerId, email));
    }
}

// In the notifications module: runs only after the order transaction commits successfully
@Component
class OrderConfirmationEmailer {

    @TransactionalEventListener
    void on(OrderPlacedEvent event) {
        System.out.println("Sending confirmation email for order " + event.orderId() + " to " + event.email());
    }
}

Because modules already communicate through public APIs and events, extracting one into a separate service later is far easier: the event becomes a message on a broker, and the method call becomes an HTTP or gRPC call.

11. Signs You Are Outgrowing Your Monolith

  • Teams regularly wait for each other to release, and deployments happen less often because they are risky.
  • Builds and test runs take so long that developers lose focus waiting for them.
  • One part of the system has very different needs, for example heavy CPU work for video processing, or far higher traffic than everything else.
  • A failure in a non-critical feature keeps bringing down critical ones such as checkout.
  • Different parts of the system genuinely need different technologies or release cycles.

Even then, the first step is often to improve modularity inside the monolith. Extract a service only for the specific module that needs independence, using the strangler fig approach described in the comparison tutorial.

12. Real-World Examples

  • Start-up products: most begin as a single Rails, Django, Laravel or Spring Boot application, because it is the fastest way to reach customers.
  • Large modular monoliths: some very large e-commerce and developer platforms have publicly described running a single, carefully modularised codebase that serves enormous traffic, investing in module boundaries instead of splitting into hundreds of services.
  • Lean, high-traffic sites: some well-known question-and-answer and community sites have published that they serve very large audiences from a monolithic application on a small number of powerful servers, with heavy caching.
  • Internal business systems: HR, accounting, inventory and admin tools are frequently, and sensibly, monoliths.

13. Common Mistakes to Avoid

  • No internal boundaries: letting any class use any other class, until nothing can be changed safely.
  • Shared "god" entities: one huge Customer or Order class used by every feature, with fields for everyone's needs.
  • Storing session state in memory: this prevents running multiple copies behind a load balancer.
  • Neglecting the database: missing indexes, no caching and no read replicas make the monolith look slow when the real problem is data access.
  • Splitting too early: moving to microservices because it is fashionable, before the team or the product needs it.
  • Creating a distributed monolith: splitting code into services that still share a database or must be released together.

14. Best Practices

  • Start with a modular monolith organised by business feature.
  • Give each module a clear owner, a small public API and its own database tables.
  • Enforce module boundaries with automated architecture tests.
  • Keep the application stateless so it can run as several copies behind a load balancer.
  • Invest in fast builds, a good automated test suite and continuous delivery, so deployments stay small and frequent.
  • Use caching, read replicas and indexing before considering a split for performance reasons.
  • Use feature flags to release incomplete work safely from a single codebase.
  • Monitor per-module metrics, so you know which part would benefit from extraction if the need arises.

15. Summary

  • A monolithic architecture builds, deploys and scales an application as one unit with a shared codebase and usually one database.
  • Internally it is organised in layers and, ideally, in business modules with clear boundaries.
  • Monoliths are simple to build, test, deploy and debug, with fast in-process calls and strong transactional consistency.
  • Their weaknesses appear at scale: entangled code, slow builds, coupled deployments and coarse scaling.
  • A well-designed modular monolith is an excellent default, and makes extracting services later much easier.

A monolith is not a legacy mistake; it is often the most productive architecture you can choose. Open the FAQ tab for answers to common questions about monolithic architecture.