Post

Microservices, Modular Monoliths, or Distributed Monoliths

The architecture landscape is varied, offering a multitude of options to developers. Among the most popular choices are microservices, modular monoliths, and distributed monoliths. Each architecture has its own strengths and weaknesses, making it crucial to understand the differences between them to make an informed decision.


graph LR
  subgraph  
    microservices["fa:fa-cubes Microservices"]
    modularMonolith["fa:fa-layer-group Modular Monolith"]
    distributedMonolith["fa:fa-network-wired Distributed Monolith"]
   end

Modular Monoliths

Modular monoliths are a hybrid approach that combines the benefits of monolithic architecture with modular design. The application is built as a single unit but organized into distinct modules, allowing for better separation of concerns and maintainability.


graph LR
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> moduleOne["🟦 Module 1"]
  modularMonolith --> moduleTwo["🟦 Module 2"]
  modularMonolith --> moduleThree["🟦 Module 3"]
graph LR
    subgraph deployment["Deployment"]
    eCommerceMonolith["fa:fa-box E-commerce Monolith"]
    end

    subgraph dataStore["Data Store"]
    monolithDatabase["fa:fa-database Monolith Database"]
    end

    subgraph modules["Modules"]
    productCatalogModule["fa:fa-folder Product Catalog Module"]
    orderModule["fa:fa-folder Order Module"]
    paymentModule["fa:fa-folder Payment Module"]
    userModule["fa:fa-folder User Module"]
    end

    eCommerceMonolith --> |read/write| monolithDatabase
    productCatalogModule -.-> eCommerceMonolith
    orderModule -.-> eCommerceMonolith
    paymentModule -.-> eCommerceMonolith
    userModule -.-> eCommerceMonolith


    style eCommerceMonolith fill:#add8e6,stroke:#000
    style monolithDatabase fill:#add8e6,stroke:#000
    style productCatalogModule fill:#c7f0f2,stroke:#000
    style orderModule fill:#fff2cc,stroke:#000
    style paymentModule fill:#d9ead3,stroke:#000
    style userModule fill:#f4cccc,stroke:#000
    style deployment fill:#f0f0f0,stroke:#000
    style dataStore fill:#f0f0f0,stroke:#000
    style modules fill:#f0f0f0,stroke:#000
  • Modules are organized by domain or feature, with clear boundaries and interfaces.
  • Shared database for all modules, but with well-defined access patterns.
  • Changes to one module can be deployed independently, but the entire application is deployed as a single unit.
  • Communication between modules is direct (function calls, shared memory).

Distributed Monoliths

Distributed monoliths are a variation of monolithic architecture where the application is split into multiple components that communicate over a network. While they offer some benefits of distributed systems, they can still suffer from the drawbacks of monolithic architecture.


graph LR
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> serviceA["fa:fa-server Service A"]
  distributedMonolith --> serviceB["fa:fa-server Service B"]
  serviceA --> serviceB
  serviceB --> serviceA
graph LR
    subgraph deployment["Deployment"]
    productCatalogService["fa:fa-box Product Catalog Service"]
    orderService["fa:fa-box Order Service"]
    paymentService["fa:fa-box Payment Service"]
    userService["fa:fa-box User Service"]
    end

    subgraph dataStore["Data Store"]
    sharedDatabase["fa:fa-database Shared Database"]
    end

    productCatalogService -->|read/write product data|sharedDatabase
    orderService -->|read/write order data|sharedDatabase
    paymentService -->|read/write payment data|sharedDatabase
    userService -->|read/write user data|sharedDatabase

    orderService -->|process payment|paymentService
    orderService -->|get user info|userService
    orderService -->|get product info|productCatalogService

    style productCatalogService fill:#c7f0f2,stroke:#000
    style orderService fill:#fff2cc,stroke:#000
    style paymentService fill:#d9ead3,stroke:#000
    style userService fill:#f4cccc,stroke:#000
    style sharedDatabase fill:#d5a6bd,stroke:#000 
    style deployment fill:#dae3f3,stroke:#000
    style dataStore fill:#dae3f3,stroke:#000

  • They share a single database (e.g., one big database for products, orders, users, and payments).
  • They communicate through synchronous calls (e.g., the Order Service directly calls the Payment Service to process a payment).
  • Changes to one service often require coordinated deployments of other services.

Microservices

Microservices are a distributed architecture composed of small, independent services, each responsible for a specific function. These services communicate over a network, enabling flexibility, scalability, and resilience.


graph LR
  microservices["fa:fa-cubes Microservices"] --> serviceOne["fa:fa-server Service 1"]
  microservices --> serviceTwo["fa:fa-server Service 2"]
  microservices --> serviceThree["fa:fa-server Service 3"]
graph LR
    subgraph external["External"]
        client["fa:fa-user Client"]
    end
    subgraph deployment["Deployment"]
        apiGateway["fa:fa-door-open API Gateway"]
        productCatalogService["fa:fa-box Product Catalog Service"]
        orderService["fa:fa-box Order Service"]
        paymentService["fa:fa-box Payment Service"]
        userService["fa:fa-box User Service"]
    end

    subgraph dataStore["Data Store"]
        productCatalogDB["fa:fa-database Product Catalog DB"]
        orderDB["fa:fa-database Order DB"]
        paymentDB["fa:fa-database Payment DB"]
        userDB["fa:fa-database User DB"]
    end

    subgraph messageQueue["Message Queue"]
        messageBroker["fa:fa-exchange Message Broker"]
    end
    
    client -->|API requests|apiGateway
    apiGateway -->|route requests|productCatalogService
    apiGateway -->|route requests|orderService
    apiGateway -->|route requests|paymentService
    apiGateway -->|route requests|userService

    productCatalogService -->|read/write|productCatalogDB
    orderService -->|read/write|orderDB
    paymentService -->|read/write|paymentDB
    userService -->|read/write|userDB

    orderService -.->|publish event|messageBroker
    paymentService -.->|consume event|messageBroker
    orderService -.->|get user info|userService
    orderService -.->|get product info|productCatalogService

    style apiGateway fill:#e6b8af,stroke:#000
    style productCatalogService fill:#c7f0f2,stroke:#000
    style orderService fill:#fff2cc,stroke:#000
    style paymentService fill:#d9ead3,stroke:#000
    style userService fill:#f4cccc,stroke:#000
    style productCatalogDB fill:#c7f0f2,stroke:#000
    style orderDB fill:#fff2cc,stroke:#000
    style paymentDB fill:#d9ead3,stroke:#000
    style userDB fill:#f4cccc,stroke:#000
    style messageBroker fill:#c5cae9,stroke:#000
    style deployment fill:#f0f0f0,stroke:#000
    style dataStore fill:#f0f0f0,stroke:#000
    style messageQueue fill:#f0f0f0,stroke:#000
    style client fill:#b7b7b7,stroke:#000
    style external fill:#f0f0f0,stroke:#000

  • Each service has its own dedicated database (or can share with related services, but with clear boundaries)
  • Asynchronous communication (message queues, event-driven) preferred, but can use synchronous when necessary with careful design

Scalability

Microservices excel in scalability. Individual services can be scaled up or down as demand dictates, without impacting the rest of the system. Modular monoliths, conversely, necessitate scaling the entire application. Distributed monoliths offer some scalability, but interdependencies can limit it.


graph LR
  microservices["fa:fa-cubes Microservices"] --> independentScaling["fa:fa-expand-arrows-alt Independent Scaling"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> wholeAppScaling["fa:fa-expand Whole Application Scaling"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> limitedScaling["fa:fa-expand Limited Scaling"]

Technology Flexibility

Microservices embrace technological diversity. Each service can be built using the technology stack best suited for its specific function. In contrast, modular and distributed monoliths typically rely on a unified tech stack across the entire application.

graph LR
  microservices["fa:fa-cubes Microservices"] --> diverseTech["fa:fa-code Diverse Technologies"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> singleTech["fa:fa-code Single Technology"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> singleTech

Deployment

Microservices enable independent deployment of services, allowing for faster release cycles and reduced risk. Modular monoliths require deploying the entire application, while distributed monoliths face similar challenges due to shared codebases and dependencies.

graph LR
  microservices["fa:fa-cubes Microservices"] --> independentDeployment["fa:fa-rocket Independent Deployment"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> wholeAppDeployment["fa:fa-rocket Whole Application Deployment"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> wholeAppDeployment

Team Autonomy

Microservices are often associated with autonomous teams, where each team is responsible for a specific service. Modular monoliths and distributed monoliths typically involve coordinated teams working on different modules or components.


graph LR
  microservices["fa:fa-cubes Microservices"] --> autonomousTeams["fa:fa-users-cog Autonomous Teams"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> coordinatedTeams["fa:fa-users Coordinated Teams"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> coordinatedTeams

Complexity

Microservices introduce the intricacies of distributed systems—communication overhead, data consistency challenges, and increased operational complexity. Modular monoliths, with their unified codebase, tend to be simpler to manage. Distributed monoliths, while offering the illusion of separation, often grapple with the complexities of microservices without reaping the full benefits.


graph LR
  microservices["fa:fa-cubes Microservices"] --> distributedComplexity["fa:fa-wrench Distributed Complexity"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> monolithicSimplicity["fa:fa-tools Monolithic Simplicity"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> hiddenComplexity["fa:fa-question-circle Hidden Complexity"]

Error Isolation & Resilience

In a microservices architecture, if one service fails, it doesn’t necessarily bring down the entire system. Other services can continue to function. Modular monoliths are more vulnerable; an error in one module can impact the whole application. Distributed monoliths share this vulnerability due to their tight coupling.


graph LR
  microservices["fa:fa-cubes Microservices"] --> errorIsolation["fa:fa-shield-alt Error Isolation"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> sharedRisk["fa:fa-exclamation-triangle Shared Risk"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> sharedRisk

Data Consistency

Microservices face challenges in maintaining data consistency across services due to distributed transactions and eventual consistency patterns. Modular monoliths and distributed monoliths, with shared databases, can enforce consistency more easily but may face challenges in scaling and maintaining clear boundaries.

graph LR
  microservices["fa:fa-cubes Microservices"] --> eventualConsistency["❄️  Eventual Consistency"]
  modularMonolith["fa:fa-layer-group Modular Monolith"] --> strongConsistency["🪨 Strong Consistency"]
  distributedMonolith["fa:fa-network-wired Distributed Monolith"] --> strongConsistency

How to Choose the Right Architecture

There is no definitive “best” choice among microservices, modular monoliths, and distributed monoliths. The optimal architecture depends on your project’s specific requirements, team expertise, scalability needs, and long-term goals.


graph LR
  yourProject["fa:fa-project-diagram Your Project"] --> microservices["fa:fa-cubes Microservices"]
  yourProject --> modularMonolith["fa:fa-layer-group Modular Monolith"]
  yourProject --> distributedMonolith["fa:fa-network-wired Distributed Monolith"]
  • Consider the Human Factor: Architecture decisions impact not just the code but also the people who work with it. Consider how the chosen architecture will affect your team’s collaboration, productivity, and satisfaction.
  • Start Small: Experiment with a small project or service to test the architecture’s suitability before committing to a larger implementation.
  • Iterate and Improve: Continuously evaluate and refine your architecture based on feedback and evolving project needs.
  • Stay Agile: Be prepared to adapt and evolve your architecture as your project evolves. Agile methodologies can help you respond to changing requirements and refine your architecture iteratively.

Keywords To Remember

graph 

  subgraph Characteristics["✨ "]
    teamAutonomy["fa:fa-users"]
    scalability["fa:fa-expand-arrows-alt"]
    flexibility["fa:fa-code"]
    deployment["fa:fa-rocket"]
    coupling["🔗"]
  end

  subgraph  
    microservices["fa:fa-cubes"]
    async["☕"]
    eventual-consistency["❄️"]
  end

  subgraph  
    distributedMonolith["fa:fa-network-wired"]
    sync["🕙"]
    strong-consistency["🪨"]
  end


  subgraph  [" "]
    modularMonolith["fa:fa-layer-group"]
    single["fa:fa-1"]
  end


This post is licensed under CC BY 4.0 by the author.