Monolithic vs Microservice Architecture

Monolithic
Microservices
SystemDesign
Scalability
SoftwareArchitecture
author avatar
Janmejay Shastri Software Engineer @ Infocusp
10 min read  .  17 January 2025

blog banner

Introduction

Understanding monolithic and microservice architectures is very crucial in software development because the architectural patterns significantly impact the application’s design, maintainability and scalability.

Architecture patterns may differ from project to project, depending on the project requirements and its nature. For example, Uber, Spotify use microservices architecture. Github, Atlassian, SoundCloud, etc were initially developed using monolithic architecture. Netflix, Atlassian, Amazon, etc which started with monolithic architecture, later migrated to microservices.

Let’s dive right in and see when to choose which architecture!

In this blog, we will understand monolithic and microservice architectures, and the pros and cons for each of them with the help of a basic Expense Tracker Application.

The Expense Tracker Application will have the following components:

  • User Interface - Web/Mobile/Desktop Application
  • Authentication - SignUp/SignIn and Session Management
  • Users - Storing User data
  • Transactions - Tracking user’s income/expense transactions
  • Reports - Retrieving Monthly Finance reports per user

Note: The Code Snippets for the example application use NodeJs, specifically ExpressJs.

Monolithic Architecture

The term “monolithic” means a massive single block of stone.

  • Single Codebase: Monolithic architecture is a traditional Software Development architecture which uses a single codebase to execute all the functions.
  • Unified / Single Unit: The entire application is unified and deployed as a single unit. This means that all the components and their associated functions are present in the same codebase.
  • Tight Coupling: Here, the components are tightly coupled. Failure in one component can potentially result in the entire application getting crashed.
  • Shared Memory: Monolithic applications share the same memory space which makes inter-component communication easier and faster due to no network overhead.
  • Centralized Database: A single Database instance is sufficient for all the data storage and retrieval needs.

Pros:

  • Ease of Testing: End-to-end Testing is easier since every service is unified which eliminates errors for network communication or misconfigurations for inter-service communication.
  • Deployment: Deployment is easier when multiple components are updated
  • Performance: Higher performance for simple use-cases due to in-memory communication
  • Cost: Low development costs in early phases of development and for POCs (Proof Of Concepts)
  • Data Consistency: Strong, Transactional data consistency is easier to achieve here.
  • Security: Since there are no inter-service network calls, security risk is low.

Cons:

  • Lack of Flexibility: Once an application is developed, changing the technologies is not easy. Moreover, using different technologies can become very challenging.
  • Scalability: The entire application must scale all at once. Individual Components cannot be scaled separately. Also, scaling results in replicating the application stack which increases infrastructure costs.
    • Consider a use-case where the Year-End Detailed Financial Report query experiences a surge while the other components operate at the same load. The reports component cannot be scaled independently in this case.
  • Deployment: Individual components cannot be deployed separately.
  • Debugging (Ripple Effect): Bug in one service can propagate forward in other services, making debugging complex.

Expense Tracker Application - Monolithic Architecture

image1

Project Directory Structure

/monolithic-application
├── server.js               // Main Application entry point
├── routes/
│   ├── auth.js             // Authentication routes
│   ├── users.js            // User routes
│   ├── transactions.js     // Transaction routes
│   └── reports.js          // Report routes
├── models/                 // Database Models
├── controllers/            // Business Logic
├── utils/                  // Utilities
└── public/                 // Status Files (UI)
// Main Server File
const express = require('express'): 

// Import route modules
const authRoutes = require('routes/auth');
const userRoutes = require('routes/users');
const transactionRoutes = require('routes/transactions');
const reportRoutes = require('routes/reports');

// Initialize the Express (Server) application
const app = express();
const PORT = process.env.PORT || 3000;

// Middlewares - bodyparser, cors, etc
...

// Register route handlers
app.use('/auth', authRoutes); // Authentication routes
app.use('/users', userRoutes); // User management routes
app.use('/transactions', transactionRoutes); // Transaction handling routes
app.use('/reports', reportRoutes); // Report generation routes 

// Start the server
app.listen(PORT, () => {
    console.log(`Monolithic app running on <http://localhost>:${PORT}`);
}) 

Here as you can see, everything is unified and run as a single server, and the entire application is deployed as a single unit.

Microservice Architecture

Microservice architecture is a software development approach where an application is organized as a collection of independent services that communicate with each other.

  • Independent Services: Each service has a set of functions to perform and is capable of functioning independently. Independent deployment and scaling is possible for each service.
  • Loose Coupling: Services have minimal dependencies on one another.
  • Inter-Service communication: Services communicate with each other using APIs, Remote Procedure Calls (RPCs), messaging queues. The communication can even be event-driven, for instance DynamoDB Streams - Service A writes to a DynamoDB Table and Service B is triggered whenever a record is written to DynamoDB Table.
  • Robust and Resilient: Failure in one service does not impact other services.
  • Decentralized Operations: Decentralized databases are possible here, i.e. each service manages its own database.

Pros:

  • Fast Development: Since each service is independently deployed, multiple teams can work simultaneously on different services.
  • Flexibility: Different tech stacks can be chosen for each service since it works independently.
  • Scalability: Each service can be scaled separately. Scaling for hot services becomes very easy, eliminating the need for replicating the entire application stack as in monolithic architecture.
  • Deployment: Each service can be deployed separately, and therefore the testing is limited to the service that is redeployed.
  • Robust and Resilient: Failure in one service does not impact functioning of other independent services, so the application can still continue to run even if one of the services fails.

Cons:

  • Data Consistency: Achieving data consistency is challenging as every service has its own database. Eventual consistency can be achieved here.
  • Performance: Inter-service network calls often add latency and performance overhead which is not the case in monolithic architecture.
  • Testing Complexity: Testing efforts are greater due to the inter-service communication and loose coupling.
  • Security Threats: Due to data exchange between multiple services, security risk is high.
  • Higher costs for early phases of development: Costs for the early phase of development are higher than monolithic due to complexities in configuring independent services which use independent infrastructure and also increase inter-service communication network calls.
  • Cascading Failures: Resilience in microservices is achieved via decoupling services but some services may depend on other services. If a service fails, it may cause other dependent services to fail, potentially causing the issue to propagate throughout the system.
  • Deployment Orchestration: It can be challenging to manage version compatibility, modifications and configurations across independent services as it adds complexities while deploying and orchestrating updates across multiple services.

Expense Tracker Application - Microservice Architecture

image2

/microservices-application
├── server.js               // Main Application entry point
├── package.json            // Dependencies
├── authService/            // Authentication Service
│   ├── src/
│   │   ├── server.js       // Service Entry point
│   │   ├── routes/         // Authentication routes
│   │   ├── controllers/    // Authentication controllers
│   │   ├── models/         // Authentication models
│   │   └── utils/          // Utilities
│   ├── Dockerfile
│   └── package.json
├── transactionService/     // Transaction Service
├── userService/            // User Service
├── reportService/          // Report Service
├── public/                 // Static Files (UI)
└── common/                 // Shared code across services
    ├── libs/               // Reusable libraries (e.g logging, validation)
    └── config/             // Shared configuration (e.g DB, service URLs)
// Authentication Service
const express = require('express'): 
const authRoutes = require('./routes/auth');
const { connectDB } = require('./utils/db');

const app = express();
const PORT = process.env.PORT || 3001;

// Middlewares - bodyparser, cors, etc
...

// Routes
app.use('/auth', authRoutes); // Authentication routes

// Connect to the Database
connectDB();

// Start the server
app.listen(PORT, () => {
    console.log(`Auth Service on <http://localhost>:${PORT}`);
})
// User Service
const express = require('express'): 
const userRoutes = require('./routes/users');
const { connectDB } = require('./utils/db');

const app = express();
const PORT = process.env.PORT || 3002;

// Middlewares - bodyparser, cors, etc
...

// Routes
app.use('/users', userRoutes); // User routes

// Connect to the Database
connectDB();

// Start the server
app.listen(PORT, () => {
    console.log(`User Service on <http://localhost>:${PORT}`);
})

Similarly, separate independent services for Transactions,Reports can be created as well.

In order to deploy all the services, Docker or another server which redirects to the appropriate endpoint based on the request data can be used.

// Main Server File
const express = require('express'): 
const axios = require('axios');

const app = express();
const PORT = process.env.PORT || 3000;

// Middlewares - bodyparser, cors, etc
...

// Routes: Proxy to individual services
app.use('/auth', (req, res) => {
    axios({
        method: req.method,
        url: `http://auth-service:3001/auth${req.url}`,
        data: req.body,
        headers: req.headers
    })
    .then(response => res.status(response.status).json(response.data))
    .catch(error => res.status(500).json({message: "<Error>" }));
});

app.use('/users', (req, res) => {
    axios({
        method: req.method,
        url: `http://user-service:3002/users${req.url}`,
        data: req.body,
        headers: req.headers
    })
    .then(response => res.status(response.status).json(response.data))
    .catch(error => res.status(500).json({message: "<Error>" }));
});

app.use('/transactions', (req, res) => {...});
app.use('/reports', (req, res) => {...});

// Start the server
app.listen(PORT, () => {
    console.log(`User Service on <http://localhost>:${PORT}`);
})

Conclusion

Choosing between monolithic and microservices architectures depends on the needs of your application, team, and business goals.

Monolithic architecture is simple to develop, deploy, and manage for small to medium-sized applications with limited scalability requirements. It works well for startups or smaller teams looking to deliver features quickly without the overhead of distributed systems. However, as the application grows, monoliths often struggle with scalability, maintainability, and fault isolation.

Microservices architecture, on the other hand, offers unmatched scalability, flexibility, and fault isolation, making it ideal for larger applications or businesses with complex systems and growing demands. However, the trade-offs include increased complexity in development, deployment, monitoring, and communication between services.

Key Takeaways:

  • If your application is small, simple, or in an early stage, a monolithic approach might be the best choice.
  • If you foresee growth, scalability challenges, or require frequent updates with independent deployments, microservices offer long-term benefits.

With the help of the example Expense Tracker application:

  • We demonstrated how a monolith can be refactored into a microservices architecture by identifying services, refactoring code, and setting up independent deployments.
  • The directory structures provide a practical guide for transitioning an application without disrupting existing functionality.

Ultimately, the decision should align with your team's expertise, the application's complexity, and your business goals. Whether you stick with a monolith or embrace microservices, understanding the trade-offs is key to building systems that are robust, scalable, and maintainable in the long run.