Back to Blog

Software Architecture Patterns: Choosing Between Monolith, Microservices, and Modular Monolith

Software architecture patterns in 2026 — monolith vs microservices vs modular monolith, when each applies, bounded contexts, anti-patterns, and how to evolve ar

Viprasol Tech Team
April 30, 2026
12 min read

Software Architecture Patterns: Choosing Between Monolith, Microservices, and Modular Monolith

"Should we use microservices?" is one of the most common architectural questions. The answer almost always: not yet, or not as many as you think. The modular monolith — clean internal boundaries within a single deployable unit — is the right starting architecture for the vast majority of products.

This guide covers the honest trade-offs, the patterns that work at each scale, and the signals that tell you when to evolve.


The Monolith — Not a Dirty Word

A monolith is a single deployable unit. The entire application — web layer, business logic, database access — ships and runs together.

Advantages:

  • Simplest possible deployment (one thing to deploy, monitor, debug)
  • No network latency between components
  • Transactions across all data are trivial (ACID within a single database)
  • Onboarding is straightforward — one codebase, one deployment
  • Debugging is easy — full stack trace in one place

Real examples: Stack Overflow (monolith serving 5M+ concurrent users), Basecamp (monolith, profitable at scale), Shopify (started as monolith, selective extraction over 15 years)

The reason monoliths get a bad reputation: they become monoliths without internal structure — every component talks to every other component, and changes anywhere break things everywhere. That's a big ball of mud, not a well-structured monolith.


The Modular Monolith

A modular monolith has clear internal module boundaries enforced at the code level. Modules communicate through defined interfaces — not direct database queries across module boundaries.

// Module structure: one folder per bounded context
// src/
//   modules/
//     orders/
//       OrderService.ts
//       OrderRepository.ts
//       types.ts
//       index.ts          ← Only public API of the module
//     billing/
//       BillingService.ts
//       index.ts
//     inventory/
//       InventoryService.ts
//       index.ts
//     notifications/
//       NotificationService.ts
//       index.ts

// orders/index.ts — explicit public API (everything else is private)
export { OrderService } from './OrderService';
export type { Order, CreateOrderDTO } from './types';
// OrderRepository is NOT exported — internal implementation detail

// billing/BillingService.ts — depends on order public API only
import { OrderService } from '../orders';  // ✅ Uses public interface
// import { OrderRepository } from '../orders/OrderRepository';  // ❌ Never direct internal access

Enforce boundaries with ESLint:

// .eslintrc.json
{
  "rules": {
    "no-restricted-imports": ["error", {
      "patterns": [
        {
          "group": ["*/orders/OrderRepository", "*/orders/internal/*"],
          "message": "Access orders module only through its public index.ts"
        }
      ]
    }]
  }
}

Database boundaries — the harder problem:

-- Option 1: Schema-per-module (same database, logical separation)
CREATE SCHEMA orders;
CREATE SCHEMA billing;
CREATE SCHEMA inventory;

-- orders.orders table belongs to orders module
-- billing.invoices table belongs to billing module
-- Cross-module joins go through services, not direct SQL

-- Option 2: Shared schema with ownership convention
-- orders_* tables owned by orders module
-- billing_* tables owned by billing module
-- No cross-module direct queries (enforced by code review)

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Microservices — When Appropriate

Microservices decompose the application into independently deployable services, each owning its own data.

Monolith:
[Web] → [Orders] → [Billing] → [DB]
            ↓
        [Inventory]

Microservices:
[API Gateway]
     ↓
[Order Service] ←→ [Billing Service] ←→ [Inventory Service]
   [Orders DB]       [Billing DB]         [Inventory DB]

Real advantages of microservices:

  • Independent scaling: High-traffic search service scales independently from low-traffic admin service
  • Independent deployment: Billing team deploys 10× per day without coordinating with other teams
  • Technology isolation: Search service uses Elasticsearch; rest of system uses PostgreSQL
  • Fault isolation: Failing recommendation service doesn't take down checkout

Microservices costs that are often underestimated:

  • Network latency between services (5–50ms per hop)
  • Distributed transactions require saga pattern or eventual consistency
  • Debugging across service boundaries requires distributed tracing
  • Deployment complexity multiplies with service count
  • Testing requires service mocks or a full integration environment
  • API versioning between services — internal APIs must be backward compatible

The microservices trap: Teams adopt microservices to enable independent team ownership, but then create tight coupling between services anyway (synchronous HTTP calls everywhere, shared databases). This produces the worst of both worlds.


Domain-Driven Design (DDD) Basics

DDD provides the vocabulary for thinking about module and service boundaries.

Bounded Context: A clear boundary within which a model is consistent. "Order" in the sales context means something different from "Order" in the fulfillment context.

Sales bounded context:           Fulfillment bounded context:
Order {                          Order {
  id                               id
  customerId                       warehouseId
  lineItems[]                      pickingListId
  totalValue                       shippingAddress
  discountCode                     items[]
  salesRepId                       estimatedShipDate
}                                }

Aggregate: A cluster of objects treated as a single unit for data changes. The aggregate root is the only entry point.

// OrderLine is part of the Order aggregate
// You never directly create/update an OrderLine — you go through Order
class Order {
  private _id: string;
  private _lines: OrderLine[] = [];
  private _status: OrderStatus = 'draft';

  addLine(productId: string, quantity: number, unitPrice: number): void {
    if (this._status !== 'draft') {
      throw new DomainError('Cannot add lines to a confirmed order');
    }
    this._lines.push(new OrderLine(productId, quantity, unitPrice));
  }

  confirm(): DomainEvent[] {
    if (this._lines.length === 0) {
      throw new DomainError('Cannot confirm empty order');
    }
    this._status = 'confirmed';
    return [new OrderConfirmed(this._id, this.total())];
  }

  total(): number {
    return this._lines.reduce((sum, line) => sum + line.lineTotal(), 0);
  }
}

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

When to Extract a Microservice

Not every module should become a service. Extract when you have a specific reason:

ReasonExample
Dramatically different scaling needsML inference service needs GPU; rest doesn't
Different deployment cadenceML models retrain daily; web app deploys weekly
Different tech requirementsLegacy C++ pricing engine stays separate from Node.js services
Independent team ownershipPlatform team owns infra service; product team owns feature services
Compliance isolationPayment processing must be PCI-DSS isolated

Not valid reasons to extract:

  • "It would be cleaner" (cleanliness is achievable within a modular monolith)
  • "Microservices are more scalable" (a well-structured monolith scales to billions of users)
  • "Team X wants to own service Y" (team structure alone doesn't justify the distributed systems overhead)

Architecture Evolution Roadmap

Stage 1 (0–PMF): Monolith
  - One codebase, one deployment, one database
  - Focus: shipping features, validating product
  - Don't think about services yet

Stage 2 (PMF–$5M ARR): Modular Monolith
  - Add module boundaries to the monolith
  - Extract truly independent services (e.g., ML inference, email sending)
  - Database schema per module
  - Focus: maintainability, team organization

Stage 3 ($5M–$20M ARR): Selective Extraction
  - Extract 2–5 services that have specific reasons for extraction
  - Keep the modular monolith as core
  - Focus: independent scaling for high-traffic components

Stage 4 ($20M+ ARR): Service Mesh
  - Multiple services, each with clear ownership
  - Kubernetes or ECS with service discovery
  - Focus: team autonomy, compliance isolation

Anti-Patterns

Distributed monolith: Services that must be deployed together because they're tightly coupled. The worst of both worlds.

Nano-services: A service for every function (UserEmailService, UserPasswordService, UserProfileService). Creates coordination overhead with no corresponding benefit.

Premature extraction: Extracting a service before you understand the domain boundaries. Results in wrong boundaries that are expensive to change.

Shared database between services: Services sharing a database table. Eliminates all benefits of service separation.


Clean Architecture in a Monolith

// Clean architecture layers (each layer only depends inward)

// Domain layer (innermost — no external dependencies)
class Order {
  // Pure business logic, no framework dependencies
  confirm(): Result<OrderConfirmed, ValidationError> { ... }
}

// Application layer (orchestrates domain objects)
class ConfirmOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,  // Interface only
    private readonly eventBus: EventBus,          // Interface only
  ) {}

  async execute(orderId: string): Promise<void> {
    const order = await this.orderRepo.findById(orderId);
    const events = order.confirm();
    await this.orderRepo.save(order);
    await this.eventBus.publishAll(events);
  }
}

// Infrastructure layer (implements interfaces — Postgres, Redis, etc.)
class PostgresOrderRepository implements OrderRepository {
  async findById(id: string): Promise<Order> { ... }
  async save(order: Order): Promise<void> { ... }
}

// Interface layer (HTTP controllers, CLI, etc.)
app.post('/orders/:id/confirm', async (req, res) => {
  await confirmOrderUseCase.execute(req.params.id);
  res.status(200).json({ confirmed: true });
});

This structure means your business logic is testable without a database, without HTTP, without Redis — just pure TypeScript/Python.


Working With Viprasol

We design software architectures for products at every stage — from startup monolith design through modular extraction and selective microservice decomposition.

Architecture review →
Software Development Services →
IT Consulting Services →


See Also


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.