CLOSE
Updated on 23 Mar, 202665 mins read 420 views

Monolithic Architecture (Monolith)

A monolith is a single, unified application where all modules (auth, product, payments, notifications..) are packaged and deployed together.

HLD Structure

┌──────────────────────────────────────────┐
│               Monolithic App             │
│ ┌────────────┬─────────────┬───────────┐ │
│ │  Auth      │  Orders     │ Payments  │ │
│ ├────────────┼─────────────┼───────────┤ │
│ │ Inventory  │Notifications│ Analytics │ │
│ └────────────┴─────────────┴───────────┘ │
└──────────────────────────────────────────┘
               ▲
               │ Single DB
               ▼
          ┌─────────┐
          │Database │
          └─────────┘

monolith_architecture
 

Characteristics

  • All modules share one codebase and are deployed at once
  • Inter-module communication happens via in-process function calls – extremely fast
  • One database (typically) serving the whole app
  • A single CI/CD pipeline ships the entire thing
  • Simple to develop locally – clone, run, done

Where monoliths shine: Early-stage startups, internal tools, small teams, and any system where the domain is not yet well understood.

Pros:

  1. Simple HLD and Development
    Single codebase, easier onboarding
  2. Simple Deployment
    One build -> one deployment
  3. Shared Memory & DB
    No network calls; faster function calls.
  4. Easy Local Dev
    Everything runs in one place

Cons:

  1. Single Point of failure
  2. Scaling Limitation
    Must scale entire application, even if only one module needs capacity.
  3. Tight Coupling
    A change in one module affects the whole system.
  4. Slower deployments with size
    As the app grows, deployment becomes heavy.
  5. Reduced Fault Isolation
    One bug can crush everything.
  6. Hard to implement polyglot tech stack
    Must use same language/framework.

When to Use Monolith

  • Early-stage startup / MVP.
  • Small engineering team.
  • When domain is not yet clearly defined.
  • When performance is more important than modularity.
  • When features are highly interdependent.

Microservices Architecture

A microservices architecture splits the system into independent, loosely-coupled services, each owning a specific business capability. They communicate over a network – via REST APIs, gRPC, or message queues.

Monolith Modules -> Services

HLD Structure

                 ┌───────────────┐
                 │ API Gateway   │
                 └───────┬───────┘
                         │ Route
─────────────────────────┼──────────────────────────────
                         ▼
      ┌──────────┐   ┌───────────┐   ┌───────────────┐
      │ Auth Svc │   │ Order Svc │   │ Payment Svc   │
      └─────┬────┘   └─────┬─────┘   └──────┬────────┘
            │              │                │
      ┌─────▼───┐    ┌─────▼───┐      ┌─────▼──────┐
      │ Auth DB │    │ Order DB│      │ Payment DB │
      └─────────┘    └─────────┘      └────────────┘

microservices_architecture

Other HLD components:

  • Service Discovery (Eureka/Consul)
  • API Gateway (Kong, Nginx, Zuul)
  • Load Balancers
  • Message Broker (Kafka, RabbitMQ)
  • Centralized Logging (ELK, Loki)
  • Tracing (Jaeger, Zipkin)

Characteristics

  • Each service has a single business responsibility
  • Services communicate over the network (HTTP, gRPC, AMQP)
  • Each service own its own data store – no shared database
  • Services can be deployed, scaled, and even rewritten independently
  • Technology diversity is possible – one service in GO, another in Python

Pros:

  1. Independent deployability
    Deploy only the service you modify.
  2. Independent scaling
    Scale only high-load services (ex: search or checkout)
  3. Fault Isolation
    Failure in one service does not kill the entire system.
  4. Polyglot flexibility
    Different services can use different languages/DB.
  5. Clear Domain Separation
    Each service is complete bounded context.

Cons:

  1. Complex HLD
    Requires DevOps maturity (gateway, CI/CD, logs, tracing).
  2. Distributed System Challenges
    Network issues, retries, timeouts, async communication.
  3. Eventual Consistency
    Synchronous consistency becomes harder
  4. Operational Overhead
    Many repositories, many deployments.
  5. Cross-cutting Concerns
    Need centralized logging, monitoring, metrics.

When to Use Microservices

  • Application is large and rapidly growing.
  • Teams are big and work on independent modules
  • Need independent deployments.
  • Different modules need different scaling modules.
  • Need high availability
  • Domain boundaries are clear
image-579.png

The hardest Problem in microservices

Microservices promise scalability, flexibility, and independent deployments. But they also introduce a set of challenges that are far more complex than what you face in a monolithic system.

1 Distributed Transactions

In a monolith, managing transactions is straightforward:

  • You wrap multiple operations in a single database transaction
  • Either everything succeeds, or everythign rolls back (ACID guarantees)

Example:

  • Create order
  • Deduct inventory
    • Both succeed or both fail

Problem in Microservices

In a microservices architecture:

  • Order Service has its own database
  • Inventory Service has itw own database

You can't perform a single ACID transaction across both.

Solution: Saga Pattern

Instead of one global transaction:

  • Break it into multiple local transactions
  • Each step has a compensating action (rollback)

Flow:

  1. Create order
  2. Deduct inventory
  3. If step 2 fails -> cancel order

Why it's Hard:

  • No automatic rollback
  • You must manually handle failures
  • System can enter inconsistent states temporarily
  • Requires careful orchestration or choreography

2 Network Latency & Partial Failures

In an monolith:

  • Function calls happen in-process (nanoseconds)
  • Failures are predictable and easier to handle

Problem in Microservices:

In distributed systems:

  • Calls happen over the network (milliseconds)
  • Networks can:
    • Fail
    • Timeout
    • Be slow

Worst case:

Service A calls Service B, times out…. but B may have already processed the request.

Why It's Hard:

  • You don't know the true state of the system
  • Retrying blindly can cause duplicate actions
  • Failures are partial, not absolute

Required Solutions

  • Circuit Breakers -> stop repeated failures
  • Retries with Idemopotency -> safe re-execution
  • Timeouts -> avoid hanging requests
  • Fallbacks -> graceful degradation

3 Observability

In a monolith:

  • One codebase
  • One log stream
  • One stack trace

Debugging is relatively straightforward

Problem in Microservices:

A single requirest might:

  • Travel across 5-10 services
  • Generate logs in multiple systems

When something breaks:

Where did it fail?

Why It's Hard

  • Logs are distributed
  • Failures are indirect
  • No single place to trace execution

Required Solutions:

  • Distributed tracing (track request across services)
  • Centralized logging (aggregate logs)
  • Metrics & monitoring dashboards
  • Correlation IDs to tie everything together

Different Phases of a Microservice

When people say “phases of microservices”, they usually mean the lifecycle of adopting and maturing a microservices architecture – not just building services, but evolving the whole system.

Microservices are not just “small services” – they requires a full lifecycle of phases to design, build, operate, and evolve.

Below are the essential phases:

1 Decomposition Phase

This is the first and most critical phase.

monolith -> microservice

This tells us if you have a monolith application, then how will you break it down to the microservice architecture.

decomposition_phase

The decomposition phase decides how the system is split into services:

There are two design patterns for decomposition:

  1. Decomposition by business logic
  2. Decomposition by sub domain

1 Decomposition by business logic

Each service is designed around a business capability – a function the business performs. Identify the entities of the business then breakdown according to them.

Say: Zomato handles orders, payments, users, restaurants.

Then we break these business logic into microservice.

OrderService
PaymentService
UserService
RestaurantService

All are deployed on different servers, all communicate through http calls.

Examples:

  • Payment Service
  • Order Service
  • Inventory Service
  • User Service

decomposition_by_business_logic
 Disadvantages:

  • Developer should be aware of entire business. However it is better for the entire team.

2 Decomposition by Subdomain

Split system based on bounded contexts in Domain Driven Design. We break sub domains.

decomposition_by_subdomain
 3 Decompose by Strangler Pattern (Incremental Migration)

It is not the other type but an implementation of the above two types of decomposition.

It answers “How will you do it?”

“Don't replace the sytem – replace it piece by piece.”

When migrating monolith -> microservices, we change it step by step.

Steps:

  1. Start with the existing monolith

    All traffic goes to the old system.

    Client -> Monolith
  2. Introduce a routing layer (proxy/gateway)

    This sits in front of your system and decides:

    - what goes to the monolith

    - what goes to new services

    Client -> Gateway -> Monolith
  3. Build a new service for one feature

    Pick a small, low-risk component

    - e.g., Notifications, Search, Auth

  4. Redirect traffic gradually

    Client -> Gateway -> New Service (for some routes)
                      -> Monolith (rest)
  5. Expand service by service
    1. Keep Extracting:
      1. Orders
      2. Payments
      3. Users
  6. Decommission the monolith

    Eventually:

    Client -> Gateway -> Microservices only

2 Database Phase

Once we have decomposed the system into services, the next step is to decide how each service handles its data.

This phase focuses on:

  • Data ownership
  • Database per service
  • Schema design
  • Consistency model
  • Data communication between services
  • Transaction patterns
  • Query patterns

Each microservice can have:

Shared DB (avoid):

All microservices share one database. This gives us free cross-table JOINs, easy ACID transactions. The problem surfaces when we try to scale or evolve independently.


shared_db

Advantages:

  • Simple to carry operations
  • Join (SQL)
  • Transaction manage (ACID)

Disadvantages:

  • Can not be scaled independently.
  • Has the limitation of either being SQL or NoSQL. Individual service has no choice of DB model selection.

Unique DB (ideal):

Every service has their own database. They can't call each other's DB directly. They have to call the respective service to get data. For instance; order service can't call the payment's DB directly. However it can get data by calling the payment service.

unique_db
Advantages:

  • Flexibility for every service to choose the DB model according to their need.
  • Service database can be scaled independently. 

Disadvantages:

  • JOIN
    • If we two services named restaurants and orders, and we have to find how many orders placed in a restaurant, then we got to use the JOINS but we can't because their DB is separate and might be deployed on different servers and their model could also be different one could be SQL other could be NoSQL.
    • We have Design Pattern CQRS for this problem.
  • Transaction Management (How to know commit, rollback across different services)
    • Normal Flow: User create an order, change is done in order DB then it payment's DB and at last at the Restaurant's DB.
    • Abnormal Flow: If order is created and payment is cancelled, then how does the User's service get to know about this and do rollback.
    • SAGA Pattern solves this problem.

3 Communication

How two microservices communicate with each other, whether through APIs or event driven system.

4 Deployment

How your microservices will be deployed. It is handled by the DevOps through CI/CD.

5 Observability

How will we monitor the application. It is also handled by the DevOps through CI/CD.

Canary Deployment

Canary deployment is a release strategy where you roll out a new version of your application to a small subset of users first, instead of everyonw at once.

If everything works fine, you gradually release it to all users.

If something breaks, you roll it back.

How It Works

  1. Deploy new version (v2) to a small group (e.g., 5% users)
  2. Remaining users still use the old version (v1)
  3. Monitor:
    1. Errors
    2. Latency
    3. User behavior
  4. If stable -> increase traffic (20% -> 50% -> 100%)
  5. If issues -> rollback to v1

Why the Name “Canary”?

Comes from the historical use of canary birds in coal mines:

  • If toxic gas appeared, the canary would be affected first
  • Warning humans early

Similarly, a small user group “tests” the release.

Key Components

  • Load Balancer: Routes a % of traffic to new version
  • Monitoring System: Detects failures
  • Features Flags: Enable/disable features dynamically

SAGA Pattern (Solves Transaction Management)

Saga Pattern is a distributed transaction management pattern used in microservices to maintain eventual consistency across multiple services without using distributed locks or 2-phase commit.

A Saga is a sequence of local transactions across multiple services.

Each step:

  • Updates its own database
  • Publishes an event (or triggers next step)

If something fails:

  • Execute compensating transactions (undo steps)

In a monolith:

BEGIN TRANSACTION
  update orders
  update payments
  update inventory
COMMIT

Works because:

  • Single DB
  • ACID guarantees

In microservices:

  • Each service has its own database.
  • No global transaction

So this breaks:

Order DB ≠ Payment DB ≠ Inventory DB

You cannot do a single ACID transaction across them.

Example: Order System

Services:

  • Order Service
  • Payment Service
  • Restaurant Service

Successful Flow:

1. Order Created, Change in its DB and call the next service
2. Payment Deducted, Change in its DB and call the next service
3. Restaurant Receives Order, Change in its DB
4. Order Confirmed

Failure Case:

If payment fails:

1. Order Created ✅
2. Payment Failed ❌, It will rollback its own state, but what about the order service.
	How order service will get this information.

SAGA solves this problem using Event Driven System.

Why Saga Is Needed

In microservices:

  • Each service has its own DB
  • Distributed transactions are slow and risky
  • Network failures are common

Saga solves this by splitting a transaction into steps, with compensating actions to undo a step if later steps fail.

Definition:

A Saga is a sequence of local transactions where each local transaction:

  • updates data within its own service
  • publishes an event or sends a command to trigger the next step

If a step fails:

  • A compensating transaction is executed to undo the previous steps.

Beak a transaction into a sequence of local transactions + compensating actions

Instead of:

All succeed or all fail (ACID)

You do:

Step-by-step execution + rollback via compensation

Two Types of SAGA

Saga has two major architecture patterns:

1 Choreography Saga (Event-Driven)

Microservices communicate via events. No central controller. Services react to events.

Flow:

Order Service  →  Inventory Service  →  Payment Service  →  Shipping Service

Each Service:

  • listens for an event
  • performs its local transaction
  • emits the next event

Example Flow (E-commerce):

  1. Order Service
    1. Local transaction: create order
    2. Emit event: OrderCreated
  2. Inventory Service
    1. On OrderCreated: reserve stock
    2. Emit: InventoryReserved
  3. Payment Service
    1. On InventoryReserved: ship item
    2. Emit: OrderShipped

Failure Example:

If payment fails:

  • Emit PaymentFailed
  • Inventory Service listens -> rollback reservation
  • Order Service listens -> cancel order

Pros:

  • Simple
  • Decentralized

Cons:

  • Hard to track flow
  • Debugging is difficult

2 Orchestration Saga (Centralized Coordinator)

A Saga Orchestrator or “workflow engine” controls the flow.

A central orchestrator controls the flow.

Flow:

Saga Orchestrator → Auth Service
                  → Payment Service
                  → Inventory Service
                  → Shipping Service

Example (E-commerce):

  1. Orchestrator sends: CreateOrder
  2. Order Service returns: OrderCreated
  3. Orchestrator sends: ReserveInventory
  4. Inventory -> InventoryReserved
  5. Orchestrator -> ProcessPayment
  6. Payment -> PaymentCompleted
  7. Orchestrator -> ShipOrder

If payment fails:

  • Orchestrator sends: ReleaseInventory
  • Orchestrator sends: CancelOrder

CQRS (Command Query Responsibility Segregation) (Solves the JOIN problem)

CQRS is an architectural pattern that separates write operations (commands) from read operations (queries) instead of using the same data model for both.

Command = Write operations (update, delete, insert)

Query = Read operations (select)

The Core Problem in Microservices

In a monolith:

SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id;

Works because:

  • Single database
  • Joins are easy

In microservices:

  • User Service -> has its own DB
  • Order Service -> has its own DB

You cannot join across databases/services directly.

How CQRS Helps?

1 When data is written (Command side)

Example:

  • User places an order

Order Service saves:

{
  "order_id": 1,
  "user_id": 42
}

2 Event is published

Order Service emits event:

OrderCreated(user_id=42, order_id=1)

3 Read model is built (Query side)

A separate Read Database is created like:

{
  "order_id": 1,
  "user_name": "Rahul",
  "order_total": 500
}

This DB is denormalized

It already has everything – No JOIN needed

Analogy

Think of it like:

  • Microservices = separate shops
  • You need a combo meal (burger + drink)

Without CQRS:

  • You go to 2 shops every time

With CQRS:

  • Someone already packed a combo for you 🍔🥤

Trade-offs

Pros:

  • Fast reads (no joins)
  • Scales well
  • Works with microservices

Cons:

  • Data duplication
  • Eventual consistency (data might be slightly delayed)
  • More complexity

Circuit Breaker

Scenario

  • Service A depends on Service B
  • Service B becomes slow or goes down

Now A keeps doing:

A → B ❌
A → B ❌
A → B ❌

Every request still tries to call B

Why This Is Dangerous

1 Slow Response (Timeout Storm)

Each request:

  • Waits for timeout
  • Blocks threads/resources

System becomes slow

2 Resource Exhaustion

  • Threads pile up
  • Connection pools fill
  • CPU/memory usage spikes

Service A can crash – even though it's healthy

3 Cascading Failures

  • Service A slows down
  • Now Service C (which depends on A) also slows

Failure spreads across system

4 No Fast Failure

Without circuit breaker:

Request → wait → timeout → fail

Every failure is slow and expensive

The real problem is not just failure – it's continuing to hit a failing service repeatedly

How Circuit Breaker Fixes It

Step 1: Detect Failure Pattern

  • Too many errors / timeouts
  • Failure threshold crossed

Step 2: “Open the Circuit”

Stop calling the failing service:

A → ❌ (blocked immediately)

No network call

No waiting

Step 3: Fail Fast

Instead of waiting for timeout

  • Return error instantly
  • Or use fallback

Step 4: Recovery (Half-Open)

After some time:

  • Allow a few test requests

If successful:

  • Resume normal flow

Before vs After

Without Circuit Breaker

A → B (slow/failing)
→ timeout
→ retry
→ timeout
→ system overload 💥

With Circuit Breaker

A → B (fails repeatedly)
→ circuit opens
→ A stops calling B
→ fast failure ⚡
→ system stable
Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy