DevToolBoxฟรี
บล็อก

Microservices vs Monolith: เลือกอะไรเมื่อไหร่ (2026)

14 นาทีในการอ่านโดย DevToolBox

Microservices vs Monolith Architecture: When to Choose What

Choosing between microservices and monolith architecture is one of the most consequential decisions you will make when designing a software system. This decision affects your development speed, deployment pipeline, team structure, operational complexity, and long-term maintainability. In 2026, the industry has matured past the hype cycle where microservices were treated as a silver bullet. Engineers now understand that both architectural styles have clear strengths and trade-offs, and the right choice depends on your team size, product stage, domain complexity, and operational capabilities.

This guide provides a thorough comparison of monolithic and microservices architectures, complete with code examples, decision frameworks, migration strategies, and real-world lessons from teams that have navigated both paths. Whether you are starting a greenfield project or considering decomposing an existing monolith, this article will help you make an informed decision.

What Is a Monolith?

A monolithic architecture packages all application functionality into a single deployable unit. The user interface, business logic, data access layer, and background jobs all live in one codebase, share one process, and are deployed together. When you run the application, every module starts up, and when you deploy a change to any part, the entire application is redeployed.

A well-structured monolith is not the same as a big ball of mud. You can organize a monolith into clean modules with clear boundaries, dependency injection, and layered architecture. The key distinction is that these modules run in the same process and are deployed as one unit.

Monolith Project Structure Example

# Well-structured monolith (Node.js / Express)
my-app/
├── src/
│   ├── modules/
│   │   ├── users/
│   │   │   ├── users.controller.ts
│   │   │   ├── users.service.ts
│   │   │   ├── users.repository.ts
│   │   │   ├── users.model.ts
│   │   │   └── users.routes.ts
│   │   ├── orders/
│   │   │   ├── orders.controller.ts
│   │   │   ├── orders.service.ts
│   │   │   ├── orders.repository.ts
│   │   │   ├── orders.model.ts
│   │   │   └── orders.routes.ts
│   │   ├── payments/
│   │   │   ├── payments.controller.ts
│   │   │   ├── payments.service.ts
│   │   │   └── payments.routes.ts
│   │   └── notifications/
│   │       ├── notifications.service.ts
│   │       └── notifications.worker.ts
│   ├── shared/
│   │   ├── database.ts
│   │   ├── logger.ts
│   │   ├── middleware/
│   │   └── utils/
│   └── app.ts
├── package.json
└── Dockerfile

Monolith Application Bootstrap

// app.ts — Single Express application with all modules
import express from 'express';
import { connectDatabase } from './shared/database';
import { userRoutes } from './modules/users/users.routes';
import { orderRoutes } from './modules/orders/orders.routes';
import { paymentRoutes } from './modules/payments/payments.routes';
import { errorHandler } from './shared/middleware/error-handler';
import { requestLogger } from './shared/middleware/request-logger';

const app = express();

app.use(express.json());
app.use(requestLogger);

// All modules share the same process
app.use('/api/users', userRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api/payments', paymentRoutes);

app.use(errorHandler);

async function start() {
  await connectDatabase();
  app.listen(3000, () => {
    console.log('Monolith running on port 3000');
  });
}

start();

What Are Microservices?

A microservices architecture decomposes an application into a collection of small, independently deployable services. Each service owns a specific business capability, runs in its own process, manages its own data store, and communicates with other services over the network using APIs, message queues, or event streams. Services can be written in different programming languages, use different databases, and be deployed on different schedules.

The core principles of microservices include single responsibility per service, independent deployability, decentralized data management, and design for failure. Each service is small enough to be owned by a single team and can be replaced or rewritten without affecting the rest of the system.

Microservices Architecture Example

# Microservices — Each service is a separate project
services/
├── user-service/           # Port 3001, owns users DB
│   ├── src/
│   │   ├── app.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.repository.ts
│   ├── Dockerfile
│   └── package.json
├── order-service/          # Port 3002, owns orders DB
│   ├── src/
│   │   ├── app.ts
│   │   ├── order.controller.ts
│   │   ├── order.service.ts
│   │   └── order.events.ts
│   ├── Dockerfile
│   └── package.json
├── payment-service/        # Port 3003, owns payments DB
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── notification-service/   # Port 3004, event-driven
│   ├── src/
│   ├── Dockerfile
│   └── package.json
├── api-gateway/            # Port 80, routes to services
│   ├── src/
│   ├── Dockerfile
│   └── package.json
└── docker-compose.yml      # Local orchestration

Inter-Service Communication

// order-service/src/order.service.ts
// Synchronous: REST call to user-service
import axios from 'axios';

const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3001';

export class OrderService {
  async createOrder(userId: string, items: OrderItem[]) {
    // Validate user exists via HTTP call
    const userResponse = await axios.get(
      `${USER_SERVICE_URL}/api/users/${userId}`
    );
    if (!userResponse.data) {
      throw new Error('User not found');
    }

    const order = await this.orderRepository.create({
      userId,
      items,
      total: this.calculateTotal(items),
      status: 'pending',
    });

    // Asynchronous: Publish event to message broker
    await this.eventBus.publish('order.created', {
      orderId: order.id,
      userId,
      total: order.total,
      items: order.items,
    });

    return order;
  }
}

// notification-service/src/event-handler.ts
// Subscribes to events from other services
import { EventBus } from './event-bus';

export class NotificationHandler {
  constructor(private eventBus: EventBus) {
    this.eventBus.subscribe('order.created', this.onOrderCreated.bind(this));
    this.eventBus.subscribe('payment.completed', this.onPaymentCompleted.bind(this));
  }

  async onOrderCreated(event: OrderCreatedEvent) {
    await this.emailService.send({
      to: event.userId,
      template: 'order-confirmation',
      data: { orderId: event.orderId, total: event.total },
    });
  }

  async onPaymentCompleted(event: PaymentCompletedEvent) {
    await this.emailService.send({
      to: event.userId,
      template: 'payment-receipt',
      data: { amount: event.amount },
    });
  }
}

Detailed Comparison

DimensionMonolithMicroservices
DeploymentSingle unit, all-or-nothingIndependent per service
ScalingEntire app scales togetherScale individual services
Data ManagementSingle shared databaseDatabase per service
Team StructureCross-functional on one codebaseTeams own individual services
ComplexityIn-process, simple debuggingNetwork calls, distributed tracing
Initial VelocityFast — no infrastructure overheadSlower — need service mesh, CI/CD per service
Fault IsolationOne bug can crash entire appFailures contained to one service
Tech StackSingle language and frameworkPolyglot — best tool per service
TestingEnd-to-end testing is straightforwardIntegration testing across services is hard

Advantages of a Monolith

Monoliths have several significant advantages that are often undervalued by engineering teams chasing the latest trends. Understanding these strengths is crucial for making an honest architectural assessment.

Simplicity of development. A single codebase means one IDE project, one build pipeline, one test suite, and one deployment process. Developers can understand the entire system, trace code paths through the debugger, and refactor across module boundaries with compiler support. There is no need for service discovery, API versioning between internal services, or distributed transaction management.

Data consistency. With a single database, you get ACID transactions across all your domain entities. Creating an order, updating inventory, and charging a customer can all happen in a single database transaction. In microservices, this requires the Saga pattern or two-phase commits, adding enormous complexity.

Operational simplicity. You monitor one application, manage one set of logs, and troubleshoot one deployment. There is no need for service meshes like Istio, distributed tracing tools like Jaeger, or container orchestration platforms like Kubernetes.

Performance. In-process function calls are orders of magnitude faster than network calls between services. A monolith avoids serialization overhead, network latency, and the need for circuit breakers and retries.

Advantages of Microservices

Independent deployment. Each service can be deployed on its own schedule. The payments team can deploy three times a day without coordinating with the notifications team. This reduces deployment risk and accelerates delivery for large organizations.

Granular scaling. If your search functionality needs 10x the compute of your user profile service, you can scale them independently. This saves significant infrastructure costs compared to scaling an entire monolith.

Fault isolation. If the recommendation engine crashes, the checkout flow continues to work. In a monolith, an out-of-memory error in the recommendations module can bring down the entire application.

Technology flexibility. The machine learning team can use Python, the API team can use Go for performance-critical services, and the frontend BFF can use Node.js. Each team picks the best tool for their specific problem domain.

Team autonomy. Microservices align well with Conway's Law. Small, autonomous teams own their services end-to-end, including development, testing, deployment, and on-call. This reduces coordination overhead and increases team ownership.

The Hidden Costs of Microservices

Before committing to microservices, you must understand the operational complexity they introduce. Many teams underestimate these costs and end up with a distributed monolith that has the worst aspects of both architectures.

Infrastructure Requirements

# docker-compose.yml — Local development becomes complex
version: '3.8'
services:
  api-gateway:
    build: ./api-gateway
    ports: ['80:80']
    depends_on: [user-service, order-service, payment-service]

  user-service:
    build: ./user-service
    ports: ['3001:3001']
    depends_on: [postgres-users, redis]
    environment:
      - DATABASE_URL=postgres://postgres:pass@postgres-users:5432/users

  order-service:
    build: ./order-service
    ports: ['3002:3002']
    depends_on: [postgres-orders, rabbitmq]
    environment:
      - DATABASE_URL=postgres://postgres:pass@postgres-orders:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq:5672

  payment-service:
    build: ./payment-service
    ports: ['3003:3003']
    depends_on: [postgres-payments, rabbitmq]

  notification-service:
    build: ./notification-service
    depends_on: [rabbitmq]

  # Each service gets its own database
  postgres-users:
    image: postgres:16
    volumes: ['pgdata-users:/var/lib/postgresql/data']

  postgres-orders:
    image: postgres:16
    volumes: ['pgdata-orders:/var/lib/postgresql/data']

  postgres-payments:
    image: postgres:16
    volumes: ['pgdata-payments:/var/lib/postgresql/data']

  rabbitmq:
    image: rabbitmq:3-management
    ports: ['5672:5672', '15672:15672']

  redis:
    image: redis:7-alpine

  # Observability stack
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports: ['16686:16686']

  prometheus:
    image: prom/prometheus
    ports: ['9090:9090']

  grafana:
    image: grafana/grafana
    ports: ['3000:3000']

volumes:
  pgdata-users:
  pgdata-orders:
  pgdata-payments:

Compare the above to a monolith that needs one application container, one database, and maybe a Redis cache. The operational burden of microservices is substantial and requires dedicated platform or DevOps engineers.

Distributed Data Challenges — The Saga Pattern

// Saga pattern for distributed transactions
// Creating an order requires coordinating multiple services

interface SagaStep {
  execute: () => Promise<void>;
  compensate: () => Promise<void>;  // Undo if later step fails
}

class CreateOrderSaga {
  private steps: SagaStep[] = [];
  private completedSteps: SagaStep[] = [];

  constructor(
    private orderService: OrderService,
    private inventoryService: InventoryService,
    private paymentService: PaymentService
  ) {
    this.steps = [
      {
        execute: () => this.orderService.createPendingOrder(),
        compensate: () => this.orderService.cancelOrder(),
      },
      {
        execute: () => this.inventoryService.reserveItems(),
        compensate: () => this.inventoryService.releaseReservation(),
      },
      {
        execute: () => this.paymentService.processPayment(),
        compensate: () => this.paymentService.refundPayment(),
      },
    ];
  }

  async execute(): Promise<void> {
    for (const step of this.steps) {
      try {
        await step.execute();
        this.completedSteps.push(step);
      } catch (error) {
        // Roll back all completed steps in reverse order
        console.error('Saga step failed, compensating...', error);
        for (const completed of this.completedSteps.reverse()) {
          await completed.compensate();
        }
        throw new Error('Order creation failed — all changes rolled back');
      }
    }
  }
}

In a monolith, the equivalent operation is a single database transaction with a try-catch and rollback. The Saga pattern adds significant complexity, and debugging failures across services requires distributed tracing infrastructure.

Decision Framework: When to Choose What

Use this framework to guide your architectural decision. Be honest about where your team and project currently stand, not where you hope to be in three years.

Choose a monolith when: your team has fewer than 20 engineers, you are building an MVP or early-stage product, your domain boundaries are not yet clear, you lack dedicated DevOps or platform engineers, you need to move fast and iterate quickly, or your application has strong data consistency requirements.

Choose microservices when: you have multiple autonomous teams (50+ engineers), different parts of the system have vastly different scaling requirements, you need independent deployment cycles for different features, your organization has mature DevOps practices and infrastructure, your domain has clear bounded contexts with minimal cross-cutting transactions, or you need technology diversity for specific problem domains.

The Modular Monolith: A Middle Ground

The modular monolith has gained popularity as a pragmatic middle ground. It provides the organizational benefits of microservices — clear module boundaries, encapsulated business logic, and independent data schemas — while retaining the operational simplicity of a monolith. Each module is designed as if it could become a microservice, but everything runs in a single process and is deployed together.

// Modular monolith — modules communicate through interfaces
// modules/users/public-api.ts (exposed to other modules)
export interface UserModule {
  getUserById(id: string): Promise<User>;
  validateUserExists(id: string): Promise<boolean>;
}

// modules/users/internal/ (private implementation)
class UserModuleImpl implements UserModule {
  constructor(private userRepo: UserRepository) {}

  async getUserById(id: string): Promise<User> {
    return this.userRepo.findById(id);
  }

  async validateUserExists(id: string): Promise<boolean> {
    const user = await this.userRepo.findById(id);
    return user !== null;
  }
}

// modules/orders/order.service.ts
// Orders module depends on UserModule interface, not implementation
class OrderService {
  constructor(
    private orderRepo: OrderRepository,
    private userModule: UserModule  // Injected interface
  ) {}

  async createOrder(userId: string, items: OrderItem[]) {
    const userExists = await this.userModule.validateUserExists(userId);
    if (!userExists) throw new Error('User not found');

    // Same-process call — no network overhead
    return this.orderRepo.create({ userId, items });
  }
}

// Dependency injection wires modules together
// app.ts
const userModule = new UserModuleImpl(new UserRepository(db));
const orderService = new OrderService(new OrderRepository(db), userModule);

When a module eventually needs to become a separate service, you replace the in-process implementation with an HTTP client that implements the same interface. The consuming code does not change.

Migration Strategy: Monolith to Microservices

If you determine that microservices are the right path, do not attempt a big-bang rewrite. The strangler fig pattern is the safest migration strategy. You incrementally extract services from the monolith, routing traffic through a facade that delegates to either the monolith or the new service.

// API Gateway implementing the Strangler Fig pattern
// Routes are gradually migrated from monolith to new services

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// Phase 1: User service extracted — route to new service
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:3001',
  changeOrigin: true,
}));

// Phase 2: Search service extracted
app.use('/api/search', createProxyMiddleware({
  target: 'http://search-service:3004',
  changeOrigin: true,
}));

// Everything else still goes to the monolith
app.use('/api', createProxyMiddleware({
  target: 'http://monolith:3000',
  changeOrigin: true,
}));

app.listen(80);

Migration Steps

Step 1: Identify the module with the clearest boundaries and the least cross-cutting concerns. User authentication, search, or notifications are often good first candidates. Step 2: Create the interface boundary in the monolith first, making the module communicate through a well-defined API within the monolith. Step 3: Extract the module into a separate service and replace the in-process calls with HTTP or message queue communication. Step 4: Route traffic through the gateway, running both old and new implementations in parallel and comparing results. Step 5: Decommission the old module from the monolith once the new service is stable.

Observability in Microservices

Microservices require robust observability. A single user request may fan out across five or more services, and you need to trace that entire journey to debug issues. The three pillars of observability are logs (structured JSON with correlation IDs), metrics (latency, error rates, throughput per service), and traces (distributed traces showing the full request path).

// Distributed tracing with OpenTelemetry
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

async function createOrder(req: Request) {
  // Start a new span — automatically linked to parent span from headers
  return tracer.startActiveSpan('createOrder', async (span) => {
    try {
      span.setAttribute('user.id', req.body.userId);
      span.setAttribute('order.items.count', req.body.items.length);

      // Child span for user validation
      const user = await tracer.startActiveSpan('validateUser', async (childSpan) => {
        const result = await userServiceClient.getUser(req.body.userId);
        childSpan.setAttribute('user.found', !!result);
        childSpan.end();
        return result;
      });

      // Child span for payment processing
      await tracer.startActiveSpan('processPayment', async (childSpan) => {
        await paymentServiceClient.charge(user.id, req.body.total);
        childSpan.end();
      });

      span.setStatus({ code: SpanStatusCode.OK });
      return { success: true };
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

Common Anti-Patterns to Avoid

Distributed monolith. If all your services must be deployed together, share a database, or fail as a group, you have a distributed monolith. You have all the complexity of microservices with none of the benefits.

Nano-services. Splitting into too many tiny services creates excessive network overhead and operational burden. A service should represent a meaningful business capability, not a single CRUD endpoint.

Shared database. If multiple services read and write to the same database tables, you have not achieved data independence. Changes to the schema will require coordinated deployments, eliminating the key benefit of independent deployability.

Synchronous chain calls. If Service A calls Service B, which calls Service C, which calls Service D, you have a brittle chain where any failure cascades. Use asynchronous messaging for operations that do not need an immediate response.

Real-World Guidance for 2026

Start with a well-structured modular monolith. Design your modules with clear boundaries and explicit interfaces as if they could become services. Invest in automated testing, CI/CD, and observability from day one. Extract services only when you have a concrete reason: a specific module needs independent scaling, a team needs deployment autonomy, or a particular problem domain requires a different technology stack.

The most successful engineering organizations in 2026 are pragmatic. They use monoliths where simplicity wins and microservices where scale demands it. They avoid premature decomposition and respect the operational cost of distributed systems. The best architecture is the one that lets your team deliver value to users reliably and quickly at your current scale — not the one that prepares you for a scale you may never reach.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

🐳Docker Compose Generator{ }JSON FormatterYMLYAML Validator & Formatter

บทความที่เกี่ยวข้อง

Kubernetes สำหรับผู้เริ่มต้น: คู่มือฉบับสมบูรณ์ (2026)

เรียนรู้ Kubernetes ตั้งแต่เริ่มต้น Pod, Service, Deployment และอื่นๆ

คู่มือ Docker Networking: เครือข่าย Bridge, Host และ Overlay

คู่มือเครือข่าย Docker ฉบับสมบูรณ์: bridge, host, overlay และ macvlan