Antwa CodeAntwaCode Blog

Rekayasa10 menit baca

Microservices vs Monolith: Pilih yang Mana?

Perbandingan microservices dan monolithic architecture.

Baca dalam English


Pendahuluan

Hampir setiap developer pernah terlibat dalam diskusi panas soal arsitektur: microservices atau monolith? Kalau kamu pernah googling topik ini, kemungkinan besar kamu malah makin bingung karena masing-masing kubu punya argumen yang sangat meyakinkan.

Yang menarik, jawabannya sebenarnya bukan "ini lebih baik dari itu." Pilihan arsitektur tergantung pada konteks — tim, produk, skala, dan roadmap bisnis kamu. Artikel ini akan bedah tuntas kedua arsitektur dari sudut pandang teknis yang realistis, bukan sekadar copy-paste dari slide presentasi vendor cloud.

Mari kita mulai dari dasar.


Apa Itu Monolith?

Monolith adalah arsitektur di mana seluruh aplikasi — mulai dari UI, bisnis logic, hingga data access — berjalan dalam satu unit deployment yang terintegrasi. Semua kode dideploy bersamaan, berjalan di satu proses (atau beberapa instance dari proses yang sama), dan berbagi database yang sama.

Karakteristik Monolith

  • Satu repository: Semua kode ada di satu tempat
  • Satu deployment unit: Build sekali, deploy sekali
  • Shared database: Semua modulis akses database yang sama
  • Tightly coupled: Modulis saling bergantung melalui function call langsung
  • Horizontal scaling: Scale dengan menjalankan banyak instance aplikasi yang sama

Contoh Monolith yang Terkenal

GitHub awalnya adalah monolith besar — Ruby on Rails yang menghandle semua fitur dari repository management, pull requests, hingga integrasi. MySQL jadi database utama yang digunakan oleh semua modul.

Lalu ada Shopify, yang masih menjalankan monolith Rails besar di core-nya meskipun sudah melayani jutaan merchant. Mereka terbukti bahwa monolith bisa scale kalau dikelola dengan benar.

Dan yang paling klasik: Basecamp/37signals selalu konsisten memilih monolith untuk produk mereka, termasuk Hey email, dengan alasan yang sangat pragmatis — tim kecil, fokus ke produk, tidak perlu complexity yang tidak perlu.


Apa Itu Microservices?

Microservices adalah arsitektur di mana aplikasi dipecah menjadi layanan-layanan kecil yang mandiri. Setiap layanan:

  • Bertanggung jawab atas satu bounded context (bisnis capability)
  • Memiliki database sendiri
  • Bisa di-deploy secara independen
  • Berkomunikasi melalui API (HTTP/gRPC) atau message queue
  • Bisa ditulis dalam bahasa/teknologi yang berbeda

Karakteristik Microservices

  • Polyglot: Tiap service bisa pakai bahasa/framework yang beda
  • Decentralized data: Setiap service punya database sendiri
  • Independent deployment: Deploy service A tanpa harus deploy service B
  • Isolation: Error di satu service tidak langsung mengambil seluruh aplikasi
  • Team autonomy: Tim kecil mengowned satu atau beberapa service

Contoh Microservices di Dunia Nyata

Netflix adalah poster child microservices. Mereka memecah sistem jadi lebih dari 1.000 microservices yang melayani ratusan juta user. Setiap layanan — recommendation engine, billing, streaming, search — berjalan mandiri di AWS.

Amazon juga mengadopsi microservices sejak awal 2000-an. Mereka famously menerapkan aturan "two-pizza team" — setiap tim cukup kecil untuk bisa diberi makan dengan dua pizza, dan setiap tim mengowned service mereka sendiri.

Grab, ride-hailing company asal Asia Tenggara, juga menggunakan arsitektur microservices untuk menangani payment, ride matching, food delivery, dan puluhan layanan lainnya secara independen.


Perbandingan Head-to-Head

1. Kompleksitas Awal vs Kompleksitas Jangka Panjang

Monolith: Kompleksitas awal rendah. Kamu cukup bikin satu project, satu database, dan mulai coding. Tapi seiring pertumbuhan, kompleksitas tumbuh secara kuadrat — lebih banyak kode = lebih banyak dependencies = lebih banyak side effect saat ubah sesuatu.

Microservices: Kompleksitas awal tinggi. Kamu perlu setup service mesh, container orchestration, distributed tracing, API gateway, service discovery, dan infrastruktur lainnya dari hari pertama. Tapi kompleksitas per service tetap terkendali karena scope-nya kecil.

Insight: Banyak startup yang gagal karena "premature microservices" — membangun infrastruktur kompleks sebelum produk mereka terbukti product-market fit.

2. Deployment

Monolith: Deploy gampang. git push, CI/CD jalan, satu artifact di-deploy. Tapi kalau ada bug di satu fitur kecil, kamu harus redeploy seluruh aplikasi. Kalau kamu sedang高峰期, satu deployment yang salah bisa mengambil seluruh sistem.

Microservices: Deploy lebih sering tapi lebih aman per service. Kamu bisa blue-green deployment atau canary release per service. Kalau ada masalah, rollback hanya service yang terdampak. Tapi kamu harus manage 10, 50, atau bahkan ratusan deployment pipelines.

3. Skalabilitas

Monolith: Scale horizontal — jalankan lebih banyak instance dari aplikasi yang sama. Masalahnya, kamu tidak bisa scale hanya modul yang hot. Kalau endpoint search yang banyak dipakai, kamu tetap harus scale seluruh aplikasi termasuk modul billing yang mungkin tidak banyak dipakai.

Microservices: Scale per service. Butuh 20 instance search service tapi cuma 2 instance billing service? Bisa. Ini jauh lebih efficient dari sisi resource utilization.

4. Database

Monolith: Satu database, join query mudah, transaksi ACID gampang diatur. Tapi semakin banyak modulis yang akses database yang sama, semakin sering terjadi contention — lock tables, slow queries yang mengambil performa modul lain, dll.

Microservices: Database per service. Tapi ini membawa tantangan baru: data yang tadinya bisa di-join dengan SQL sekarang harus dilakukan di level aplikasi atau dengan CQRS/event sourcing. Transaksi yang melibatkan multiple service jadi Distributed Transaction yang sangat kompleks.

5. Fault Tolerance

Monolith: Kalau satu modul crash, biasanya mengambil seluruh aplikasi. Tapi karena semuanya di satu tempat, debugging relatif mudah — ada satu log, satu stack trace.

Microservices: Service A crash, service B dan C masih jalan. Tapi kamu perlu circuit breaker, retry logic, fallback mechanisms, dan distributed tracing untuk debug masalah yang melibatkan multiple services.

6. Developer Experience

Monolith: Onboarding baru relatif mudah — clone satu repo, baca satu codebase, jalankan satu aplikasi. Tapi kalau codebase sudah terlalu besar, navigasi code jadi tantangan sendiri.

Microservices: Setiap service lebih mudah dipahami karena scope-nya kecil. Tapi developer harus memahami bagaimana services berinteraksi, bagaimana data flow antar service, dan seringkali harus menjalankan beberapa service sekaligus untuk development lokal (biasanya pakai Docker Compose).

7. Organisasi & Team

Monolith: Cocok untuk tim kecil (3-8 developer). Semua orang bisa kenal seluruh codebase. Coordination mudah karena semuanya dalam satu repo.

Microservices: Cocok untuk tim besar (20+). Setiap tim bisa punya service sendiri tanpa harus coordinate deployment dengan tim lain. Ini align dengan Conway's Law — sistem yang didesain mencerminkan struktur organisasi yang membuatnya.


Kapan Pilih Monolith?

✅ Monolith adalah pilihan tepat kalau:

  1. Tim kamu masih kecil (< 10 developer). Lebih baik fokus ke product daripada infrastruktur.

  2. Produk masih dalam tahap eksplorasi/validasi. Kamu belum tahu apa yang user butuhkan. Dengan monolith, pivot lebih mudah dan cepat.

  3. Kamu tidak butuh Independent Deployment. Kalau satu deployment per hari sudah cukup, monolith lebih praktis.

  4. Domain bisnis kamu relatif sederhana. Kalau aplikasi kamu adalah CRUD app dengan 5-10 tabel, microservices hanya menambah complexity tanpa manfaat nyata.

  5. Kamu ingin time-to-market cepat. Dari nol ke production, monolith bisa dicapai dalam hari/bulan, bukan bulan/tahun.

Contoh Kasus yang Cocok Monolith

  • MVP startup yang sedang validasi ide
  • Internal tools / admin panel
  • Aplikasi untuk company kecil-menengah (SaaS dengan 100-1000 user)
  • Aplikasi dengan tim developer 3-5 orang
  • Website corporate / company profile

Kapan Pilih Microservices?

✅ Microservices adalah pilihan tepat kalau:

  1. Tim sudah besar dan perlu autonomi. Kalau 20+ developer harus coordinate untuk deploy satu aplikasi, throughput development akan sangat rendah.

  2. Ada clear domain boundaries. Domain-driven design sudah mengidentifikasi bounded context yang jelas — user management, order processing, payment, notification, dll.

  3. Scalability requirement berbeda per modul. Search endpoint perlu 100x lebih banyak resource dibandingkan admin dashboard.

  4. Kamu perlu polyglot technology. Misalnya, machine learning service pakai Python, payment gateway service pakai Go, dan notification service pakai Node.js.

  5. Kamu sudah punya product-market fit dan aplikasi mulai mengalami masalah skalabilitas / deployment bottleneck di monolith.

Contoh Kasus yang Cocok Microservices

  • Platform e-commerce dengan jutaan user
  • Ride-hailing / delivery platform
  • Social media / content platform
  • Banking / fintech dengan banyak modul terpisah
  • Multi-tenant SaaS yang melayani ribuan customer

The Strangler Fig Pattern: Strategi Migrasi dari Monolith ke Microservices

Kalau kamu sudah di monolith dan merasa perlu beralih ke microservices, jangan langsung rewrite dari nol. Gunakan Strangler Fig Pattern — strategi incremental migration yang jauh lebih aman.

Konsep Dasar

Strangler Fig Pattern dinamai dari pohon fig yang tumbuh di sekitar pohon inang, perlahan menggantikannya. Dalam konteks software:

  1. Bangun microservice baru untuk satu fitur / bounded context
  2. Pasang reverse proxy atau API gateway yang mengarahkan traffic ke service baru atau monolith berdasarkan routing rule
  3. Setelah service baru stabil, pindahkan traffic secara gradual
  4. Hapus kode lama dari monolith
  5. Ulangi untuk fitur berikutnya

Langkah-langkah Praktis

Phase 1: Identifikasi dan Prioritisasi

# Analisis dependency di monolith
# Cari modul yang paling sering berubah (high churn)
git log --oneline --since="6 months ago" | \
  awk '{print $NF}' | sort | uniq -c | sort -rn | head -20

Pilih modul dengan:

  • Churn tinggi (sering berubah)
  • Dependency rendah (tidak banyak modul lain yang tergantung padanya)
  • Tim khusus yang maintain

Phase 2: Bangun Service Baru

Misalnya, kamu ingin memisahkan "notification service" dari monolith:

# notification_service/app.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
 
app = FastAPI(title="Notification Service")
 
class NotificationRequest(BaseModel):
    user_id: str
    channel: str  # email, sms, push
    template: str
    data: dict
    priority: Optional[str] = "normal"
 
@app.post("/notify")
async def send_notification(req: NotificationRequest):
    # Logic pengiriman notifikasi
    if req.channel == "email":
        result = await send_email(req)
    elif req.channel == "sms":
        result = await send_sms(req)
    elif req.channel == "push":
        result = await send_push(req)
 
    return {"status": "sent", "notification_id": result.id}
 
@app.get("/health")
async def health_check():
    return {"status": "healthy"}

Phase 3: Setup Routing di API Gateway

# api-gateway/routes.yaml
routes:
  - path: /api/notifications/**
    service: notification-service
    port: 8080
 
  - path: /api/**
    service: monolith  # fallback ke monolith
    port: 3000

Phase 4: Gradual Traffic Shifting

# Canary: mulai dari 5% traffic ke service baru
curl -X POST http://api-gateway/admin/routing \
  -d '{"path": "/api/notifications/**", "weight": 5}'
 
# Monitoring selama seminggu...
# Kalau OK, naikkan ke 25%, 50%, 100%

Phase 5: Cleanup

Setelah 100% traffic ke service baru:

  • Hapus kode notification dari monolith
  • Hapus database tables yang tidak dipakai
  • Update documentation

Distributed Systems: Kenyataan yang Harus Dihadapi

Kalau kamu pindah ke microservices, bersiaplah untuk masalah-masalah yang tidak ada di monolith.

The Eight Fallacies of Distributed Systems (Peter Deutsch)

  1. The network is reliable — Network pasti akan gagal. Sering.
  2. Latency is zero — Setiap panggilan antar service punya latency tambahan (ms, bukan µs).
  3. Bandwidth is infinite — Network bandwidth terbatas, apalagi kalau banyak data yang perlu dikirim antar service.
  4. The network is secure — Setiap endpoint antar service adalah potensi attack vector.
  5. Topology doesn't change — Service bisa di-scale, di-restart, di-pindah kapan saja.
  6. There is one administrator — Banyak tim yang mengelola service yang berbeda.
  7. Transport cost is zero — Serialization/deserialization (JSON, Protobuf) butuh CPU time.
  8. The network is homogeneous — Service bisa ditulis dalam bahasa/teknologi yang berbeda.

Masalah yang Harus Dipecahkan

1. Service Discovery

Service A perlu tahu alamat service B. Dalam monolith, tinggal panggil function. Dalam microservices, kamu perlu service registry (Consul, etcd) atau DNS-based discovery.

# Kubernetes service discovery otomatis
apiVersion: v1
kind: Service
metadata:
  name: notification-service
spec:
  selector:
    app: notification-service
  ports:
    - port: 8080
      targetPort: 8080

2. Circuit Breaker

Kalau service B down, service A tidak boleh terus-menerus retry sampai timeout. Implementasikan circuit breaker:

# Python example dengan tenacity
from tenacity import retry, stop_after_attempt, wait_exponential
 
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10)
)
async def call_notification_service(data):
    response = await httpx.post(
        "http://notification-service/notify",
        json=data,
        timeout=5.0
    )
    response.raise_for_status()
    return response.json()

3. Distributed Tracing

Ketika request melewati 5 service berbeda, debugging jadi nightmare tanpa distributed tracing. Gunakan OpenTelemetry:

# Setiap service harus propagate trace context
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
 
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://jaeger:4317"))
)
trace.set_tracer_provider(tracer_provider)
tracer = trace.get_tracer("order-service")

4. Data Consistency

Di monolith, kamu bisa pakai database transaction biasa:

BEGIN;
  INSERT INTO orders (...) VALUES (...);
  UPDATE inventory SET stock = stock - 1 WHERE product_id = ...;
  INSERT INTO payments (...) VALUES (...);
COMMIT;

Di microservices, order-service, inventory-service, dan payment-service punya database masing-masing. Kamu perlu Saga Pattern atau Event Sourcing:

Order Service: Create Order → Publish OrderCreated Event
Inventory Service: Listen OrderCreated → Reserve Stock → Publish StockReserved
Payment Service: Listen OrderCreated → Process Payment → Publish PaymentCompleted
Order Service: Listen PaymentCompleted → Confirm Order
// Kalau PaymentFailed → Compensate: Release Stock, Cancel Order

Decision Framework: Checklist Pemilihan Arsitektur

Gunakan checklist ini untuk menentukan arsitektur yang tepat untuk proyek kamu:

Scorecard Monolith (+1 each)

  • Tim < 10 developer
  • Produk masih pre-PMF / MVP
  • Domain sederhana (< 5 bounded context)
  • Tidak butuh independent scaling per modul
  • Butuh time-to-market cepat (< 3 bulan ke production)
  • Budget infrastruktur terbatas
  • Tim belum punya pengalaman distributed systems

Scorecard Microservices (+1 each)

  • Tim > 15 developer, perlu autonomous teams
  • Produk sudah post-PMF dengan user base signifikan
  • Domain kompleks dengan ≥ 5 bounded context jelas
  • Modul berbeda punya scalability requirement sangat berbeda
  • Butuh polyglot technology stack
  • Deployment frequency tinggi (multiple per day per service)
  • Tim punya DevOps / SRE capability

Skor Monolith > Microservices: Pilih monolith. Kamu bisa migrasi nanti kalau perlu. Skor Microservices > Monolith + tim punya DevOps capability: Pertimbangkan microservices. Skor seimbang: Mulai monolith, siapkan untuk strangle nanti.


Hybrid: Modular Monolith sebagai Jalan Tengah

Ada opsi ketiga yang sering terlupakan: modular monolith. Konsepnya:

  • Satu deployment unit (monolith)
  • Tapi kode diorganisir dalam modul-modul yang terisolasi
  • Setiap modul punya interface yang jelas
  • Database bisa di-split per modul atau tetap shared dengan schema separation
  • Module boundary di-enforce oleh tooling (archunit, boundary check, dll)

Contoh Struktur

src/
├── modules/
│   ├── user/
│   │   ├── api/
│   │   │   └── UserController.java
│   │   ├── domain/
│   │   │   └── User.java
│   │   ├── application/
│   │   │   └── UserService.java
│   │   └── infrastructure/
│   │       └── UserRepository.java
│   ├── order/
│   │   ├── api/
│   │   ├── domain/
│   │   ├── application/
│   │   └── infrastructure/
│   └── payment/
│       ├── api/
│       ├── domain/
│       ├── application/
│       └── infrastructure/
└── shared/
    ├── kernel/
    └── infrastructure/

Aturan Penting

  1. Modul tidak boleh akses modul lain secara langsung — hanya lewat published interface
  2. Setiap modul punya database schema sendiri (bisa shared DB instance, tapi schema terpisah)
  3. Inter-modul communication via event atau service interface, bukan direct DB access

Modular monolith memberikan 80% manfaat microservices (clean boundaries, independently testable modules) dengan 20% complexity-nya.


Real-World Migration Stories

Shopify: Monolith yang Scale

Shopify memutuskan untuk tetap dengan monolith Rails mereka. Tapi mereka melakukan refactoring besar-besaran:

  • Memecah database menjadi "cells" — setiap cell melayani subset merchant
  • Menggunakan service-oriented modul di dalam monolith
  • Memperkenalkan async processing dengan job queues
  • Membangun custom database proxy untuk horizontal scaling

Hasilnya? Monolith yang melayani jutaan merchant tetap scalable tanpa harus migrasi ke microservices.

SoundCloud: Migrasi dari Monolith

SoundCloud migrasi dari monolith Ruby on Rails ke microservices secara gradual:

  1. Mulai dengan memisahkan "playback service"
  2. Menambahkan "track service", "user service", "search service"
  3. Menggunakan REST API dan event-driven communication
  4. Migrasi memakan waktu beberapa tahun, tapi zero downtime

Kunci sukses mereka: incremental migration menggunakan Strangler Fig Pattern.

Majoo (Indonesia): Microservices dari Awal

Majoo, startup fintech Indonesia, membangun arsitektur microservices dari awal karena kompleksitas domain mereka — payment, lending, merchant management, analytics, dan puluhan fitur lainnya. Mereka menggunakan Kubernetes dan service mesh (Istio) untuk manage ratusan service di awal.


Tech Stack Rekomendasi

Untuk Monolith

KomponenRekomendasi
LanguageRuby (Rails), Python (Django), Java (Spring Boot), PHP (Laravel)
DatabasePostgreSQL, MySQL
DeploymentPaaS (Heroku, Railway, Render) atau simple VPS
MonitoringSentry (error), UptimeRobot (availability)
CI/CDGitHub Actions, GitLab CI

Untuk Microservices

KomponenRekomendasi
ContainerDocker
OrchestrationKubernetes, Docker Swarm
API GatewayKong, Traefik, Envoy
Service MeshIstio, Linkerd
Service DiscoveryK8s DNS, Consul
ObservabilityPrometheus + Grafana, Jaeger (tracing), ELK Stack
Message QueueKafka, RabbitMQ, NATS
CI/CDArgoCD, Flux, Jenkins X

Kesimpulan

Tidak ada arsitektur yang sempurna. Microservices bukan silver bullet, dan monolith bukan hal yang kuno.

Pilih monolith kalau:

  • Kamu masih mencari product-market fit
  • Tim kamu kecil
  • Domain kamu belum terlalu kompleks
  • Kamu butuh velocity tinggi

Pilih microservices kalau:

  • Tim kamu besar dan perlu autonomi
  • Setiap modul punya scalability requirement yang sangat berbeda
  • Kamu sudah punya product-market fit dan mengalami bottleneck di monolith

Pilih modular monolith kalau:

  • Kamu ingin clean boundaries tanpa complexity distributed systems
  • Tim kamu sedang tumbuh dan belum siap untuk full microservices

Yang terpenting, ingat: arsitektur yang baik adalah arsitektur yang bisa berubah. Mulai dengan yang sederhana, refactor saat ada masalah nyata, bukan masalah yang dibayangkan.

Selamat coding! 🚀


Referensi

Tulisan lain