Microservices #

Microservices adalah pendekatan arsitektur di mana sebuah aplikasi dibangun sebagai kumpulan service-service kecil yang independen — masing-masing berjalan dalam prosesnya sendiri, berkomunikasi melalui API yang terdefinisi dengan baik, dan bisa di-deploy secara terpisah. Setiap service bertanggung jawab atas satu domain bisnis yang spesifik.

Yang sering tidak disadari: microservices bukan tentang ukuran (bukan “kecil” dalam arti jumlah baris kode), tapi tentang independensi. Service yang bisa di-develop, di-test, di-deploy, dan di-scale secara independen dari service lain — itulah yang membuat arsitektur ini berharga.

Dan yang lebih sering tidak disadari: microservices membawa kompleksitas yang sangat signifikan. Tim yang belum siap menghadapi distributed systems akan menemukan dirinya memiliki semua kelemahan monolith plus semua kelemahan distributed systems — tanpa manfaat dari keduanya. Martin Fowler menyebut ini “distributed monolith” — anti-pattern paling buruk dari dua dunia.

Monolith Dulu, Microservices Kemudian #

Salah satu kesalahan paling umum dalam software engineering adalah memulai langsung dengan microservices. Ini terasa seperti keputusan yang ambisius dan forward-thinking, tapi hampir selalu berujung pada masalah.

Mengapa mulai dengan monolith lebih bijak:

  Awal project:
  → Domain bisnis belum sepenuhnya dipahami
  → Boundary yang salah sangat mahal untuk diubah di microservices
  → Tim kecil tidak butuh independensi deployment yang diklaim microservices
  → Overhead operasional microservices tidak sebanding dengan manfaatnya

  Monolith yang baik (modular monolith):
  → Kode terstruktur dengan boundary modul yang jelas
  → Modul hanya berkomunikasi melalui interface yang terdefinisi
  → Mudah untuk di-refactor menjadi microservices nanti
  → Jauh lebih mudah untuk di-develop, di-test, dan di-debug

  Kapan microservices mulai masuk akal:
  → Tim sudah cukup besar (biasanya 15+ engineer)
  → Bottleneck scaling yang jelas dan spesifik per domain
  → Deployment frequency yang tinggi dan tim yang berbeda perlu deploy independen
  → Domain bisnis sudah dipahami dengan baik (bukan assumption)
  → Tim sudah berpengalaman dengan distributed systems
graph LR
    subgraph Monolith["Modular Monolith — Mulai di sini"]
        A[User Module] --> B[Order Module]
        A --> C[Payment Module]
        B --> C
        B --> D[Inventory Module]
    end

    subgraph Microservices["Microservices — Evolusi setelah ada kebutuhan jelas"]
        E[User Service] -->|API| F[Order Service]
        F -->|API| G[Payment Service]
        F -->|API| H[Inventory Service]
        E -->|API| G
    end

    Monolith -->|Evolusi ketika ada kebutuhan nyata| Microservices

Mendefinisikan Service Boundary yang Benar #

Service boundary yang salah adalah akar dari sebagian besar masalah di arsitektur microservices. Boundary yang terlalu granular menciptakan chatty services yang saling bergantung. Boundary yang terlalu lebar kehilangan manfaat independensi.

Domain-Driven Design (DDD) memberikan kerangka kerja untuk mendefinisikan boundary yang tepat melalui konsep Bounded Context — setiap service harus sesuai dengan satu bounded context, di mana model domain memiliki arti yang konsisten dan tidak ambiguus.

Prinsip mendefinisikan service boundary:

  ✓ Sesuai dengan domain bisnis, bukan layer teknis
    Jangan: UserDatabase Service, OrderRepository Service
    Ya:     User Service (semua tentang user), Order Service (semua tentang order)

  ✓ High cohesion, loose coupling
    High cohesion: semua yang ada di service ini berkaitan erat satu sama lain
    Loose coupling: service tidak perlu tahu detail implementasi service lain

  ✓ Service punya database sendiri (database per service)
    Tidak ada sharing database antar service
    Ini yang memungkinkan independensi deployment sesungguhnya

  ✓ Service bisa di-deploy tanpa mengubah service lain
    Jika mengubah satu service selalu butuh mengubah service lain → boundary salah

  ✗ Tanda-tanda boundary yang salah:
    → Service A selalu dipanggil bersamaan dengan Service B (seharusnya satu service)
    → Mengubah model domain di Service A selalu butuh mengubah Service B
    → Satu request dari user butuh 10+ service call untuk diselesaikan (terlalu granular)
    → Service yang tidak punya state sendiri (hanya orchestrator tanpa domain logic)

Komunikasi Antar Service: Sinkron vs Asinkron #

Pilihan cara service berkomunikasi satu sama lain adalah salah satu keputusan terpenting dalam microservices.

Komunikasi Sinkron (REST/gRPC) #

# Service A memanggil Service B secara sinkron

import httpx
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential

# HTTP client dengan timeout dan retry
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10)
)
async def get_user_profile(user_id: int) -> dict:
    """
    Panggil User Service untuk mendapatkan profil user.
    Retry otomatis dengan exponential backoff jika gagal.
    """
    async with httpx.AsyncClient(timeout=5.0) as client:
        response = await client.get(
            f"http://user-service/users/{user_id}",
            headers={"X-Request-ID": get_request_id()}  # propagate trace context
        )
        response.raise_for_status()
        return response.json()

# Circuit breaker untuk menghindari cascade failure
from circuitbreaker import circuit

@circuit(failure_threshold=5, recovery_timeout=30)
async def get_inventory(product_id: int) -> dict:
    """
    Jika inventory-service down:
    - Setelah 5 kegagalan, circuit OPEN
    - Request langsung gagal (tidak perlu tunggu timeout)
    - Setelah 30 detik, coba lagi (HALF-OPEN)
    """
    async with httpx.AsyncClient(timeout=3.0) as client:
        response = await client.get(
            f"http://inventory-service/products/{product_id}/stock"
        )
        return response.json()
Kapan gunakan komunikasi sinkron:
  ✓ Response diperlukan sebelum bisa melanjutkan (query data)
  ✓ User menunggu response langsung (read operation)
  ✓ Operasi yang membutuhkan acknowledgement langsung

Risiko komunikasi sinkron:
  → Coupling temporal: jika Service B down, Service A juga gagal
  → Cascade failure: kegagalan satu service menyebar ke service lain
  → Latency akumulasi: latency = jumlah latency semua service yang dipanggil
  → Wajib: circuit breaker + timeout + retry dengan exponential backoff

Komunikasi Asinkron (Message Queue) #

# Komunikasi via message queue — loose coupling, resilient

import json
import boto3
from dataclasses import dataclass, asdict

sqs = boto3.client('sqs', region_name='ap-southeast-1')

@dataclass
class OrderCreatedEvent:
    event_type: str = "order.created"
    order_id: str = ""
    user_id: int = 0
    total_amount: float = 0.0
    items: list = None

def publish_order_created(order_id: str, user_id: int, total: float, items: list):
    """
    Publish event ke queue — Order Service tidak perlu tahu
    siapa yang akan consume event ini.
    """
    event = OrderCreatedEvent(
        order_id=order_id,
        user_id=user_id,
        total_amount=total,
        items=items or []
    )

    sqs.send_message(
        QueueUrl=os.environ['ORDER_EVENTS_QUEUE_URL'],
        MessageBody=json.dumps(asdict(event)),
        MessageAttributes={
            'EventType': {
                'StringValue': event.event_type,
                'DataType': 'String'
            }
        }
    )

# Consumer di service lain yang independen
def handle_order_created(event_data: dict):
    """
    Inventory Service consume event order.created.
    Jika Inventory Service down, pesan tetap di queue sampai bisa diproses.
    """
    order_id = event_data['order_id']
    items = event_data['items']

    for item in items:
        reserve_inventory(item['product_id'], item['quantity'])

    logger.info(f"Inventory reserved for order {order_id}")
Kapan gunakan komunikasi asinkron:
  ✓ Operasi yang tidak perlu response langsung (write/action)
  ✓ Fanout: satu event perlu dikonsumsi banyak service
  ✓ Workload yang bisa di-buffer (email, notifikasi, report generation)
  ✓ Resilience: consumer bisa down dan catch up nanti

Pattern event yang berguna:
  Event Notification: "Sesuatu terjadi" (minimal data)
    → order.created { order_id: "123" }
    → Consumer fetch detail sendiri jika butuh

  Event-Carried State Transfer: event berisi semua data
    → order.created { order_id: "123", user_id: 42, items: [...] }
    → Consumer tidak perlu call balik ke service lain

  Event Sourcing: semua state changes sebagai event
    → Rebuild state dengan replay semua event
    → Lebih kompleks tapi sangat powerful untuk audit trail

API Gateway: Pintu Masuk Tunggal #

API Gateway adalah komponen yang duduk di depan semua microservices, menyediakan single entry point untuk client.

Tanggung jawab API Gateway:

  Routing:
  → GET /users/* → User Service
  → GET /orders/* → Order Service
  → POST /payments/* → Payment Service

  Cross-cutting concerns:
  → Authentication & Authorization (validasi JWT sebelum request diteruskan)
  → Rate limiting
  → SSL termination
  → Request/response logging
  → Correlation ID injection (untuk distributed tracing)

  Client-specific aggregation:
  → BFF (Backend for Frontend): satu gateway per client type
    - Mobile BFF: response yang di-optimize untuk bandwidth rendah
    - Web BFF: response yang lebih kaya untuk desktop browser

  Manfaat:
  → Client tidak perlu tahu ada berapa service dan di mana
  → Perubahan service internal tidak mempengaruhi API yang di-expose ke client
  → Single place untuk cross-cutting concerns
# Kong API Gateway configuration (contoh)

services:
  - name: user-service
    url: http://user-service:8080

  - name: order-service
    url: http://order-service:8080

routes:
  - name: users-route
    service: user-service
    paths:
      - /api/users

  - name: orders-route
    service: order-service
    paths:
      - /api/orders

plugins:
  - name: jwt          # Authentication
    service: user-service
    config:
      key_claim_name: kid

  - name: rate-limiting
    config:
      minute: 100      # 100 request per menit per consumer
      policy: local

  - name: correlation-id
    config:
      header_name: X-Correlation-ID
      generator: uuid

Service Discovery #

Dalam microservices, service instances bisa berjalan di berbagai host dan port yang berubah-ubah (terutama di Kubernetes). Service discovery memungkinkan service menemukan satu sama lain tanpa hardcode address.

Dua pendekatan service discovery:

  Client-side discovery:
  → Client query Service Registry untuk menemukan instance
  → Client melakukan load balancing sendiri
  → Contoh: Netflix Eureka, Consul

  Server-side discovery:
  → Client hanya tahu satu endpoint (load balancer)
  → Load balancer yang tahu di mana instance-instance berada
  → Contoh: AWS ALB, Kubernetes Service

  Di Kubernetes: Service discovery built-in
  → Setiap Service dapat DNS name
  → http://user-service.default.svc.cluster.local
  → Atau cukup: http://user-service (dalam namespace yang sama)
  → kube-proxy handle load balancing ke pods yang sehat
# Kubernetes Service definition — service discovery otomatis

apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: default
spec:
  selector:
    app: user-service      # route ke pods dengan label ini
  ports:
    - protocol: TCP
      port: 80             # port yang di-expose
      targetPort: 8080     # port di dalam pod

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3              # 3 instance untuk HA
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: myregistry/user-service:v1.2.3
          ports:
            - containerPort: 8080
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: user-service-secrets
                  key: database_url
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30

Distributed Tracing #

Ketika satu request dari user melewati 5 service, debugging menjadi sangat sulit tanpa distributed tracing. Distributed tracing menghubungkan semua log dan span dari seluruh service menjadi satu trace yang bisa dilihat end-to-end.

# OpenTelemetry — standar untuk distributed tracing

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

# Setup tracer — dilakukan sekali saat startup
def setup_tracing(service_name: str):
    provider = TracerProvider()
    exporter = OTLPSpanExporter(endpoint="http://jaeger:4317")
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)

    # Auto-instrument FastAPI dan HTTP client
    FastAPIInstrumentor().instrument()
    HTTPXClientInstrumentor().instrument()

# Tracer untuk manual instrumentation
tracer = trace.get_tracer(__name__)

async def process_order(order_data: dict):
    # Semua child span akan terhubung ke parent span yang ada
    with tracer.start_as_current_span("process_order") as span:
        span.set_attribute("order.id", order_data['id'])
        span.set_attribute("order.total", order_data['total'])

        with tracer.start_as_current_span("validate_payment"):
            await validate_payment(order_data)

        with tracer.start_as_current_span("reserve_inventory"):
            await reserve_inventory(order_data['items'])

        with tracer.start_as_current_span("notify_fulfillment"):
            await publish_order_created(order_data)
Distributed tracing memberikan:

  Trace: keseluruhan perjalanan satu request dari ujung ke ujung
  Span: satu operasi dalam trace (satu service call, satu DB query)
  Context propagation: trace ID dikirim via HTTP header ke semua service

  Yang bisa dilihat:
  → Berapa waktu yang dihabiskan di setiap service
  → Di mana bottleneck terjadi
  → Error terjadi di service mana
  → Bagaimana service saling memanggil

  Tools:
  → Jaeger (open source)
  → Zipkin (open source)
  → AWS X-Ray
  → Datadog APM

Eventual Consistency: Realita Data di Microservices #

Karena setiap service punya database sendiri, tidak ada ACID transaction yang bisa span multiple services. Data antar service hanya bisa eventual consistency — pada akhirnya konsisten, tapi ada window di mana state berbeda-beda.

Mengelola eventual consistency:

  Masalah:
  Order dibuat di Order Service (status: "pending")
  Event order.created dikirim ke Payment Service
  Payment Service sedang down → belum proses
  User lihat order status: "pending" — tapi payment belum diproses
  → Data temporarily inconsistent, tapi akhirnya consistent

  Strategi:

  1. Saga Pattern — long-running transaction dengan compensating actions
     Step 1: Order Service → create order (status: pending)
     Step 2: Payment Service → charge payment
     Step 3: Inventory Service → reserve items
     Step 4: Order Service → update status ke confirmed

     Jika Step 3 gagal:
     Compensate Step 2: refund payment
     Compensate Step 1: cancel order

  2. Outbox Pattern — menghindari dual-write problem
     Alih-alih write ke DB + publish event secara terpisah:
     → Tulis ke DB dan outbox table dalam satu transaction
     → Background process publish event dari outbox table
     → Jika publish gagal, event masih di outbox → bisa di-retry
# Outbox Pattern untuk reliable event publishing

class OrderRepository:
    def create_order_with_event(self, order_data: dict) -> dict:
        """
        Buat order DAN catat event dalam satu database transaction.
        Tidak ada kemungkinan order dibuat tapi event tidak terpublish.
        """
        with db.session.begin():
            # 1. Buat order
            order = Order(**order_data)
            db.session.add(order)
            db.session.flush()  # generate order.id

            # 2. Simpan event ke outbox table dalam transaction yang sama
            outbox_entry = OutboxEvent(
                event_type='order.created',
                aggregate_id=str(order.id),
                payload=json.dumps({
                    'order_id': str(order.id),
                    'user_id': order.user_id,
                    'total': float(order.total),
                }),
                created_at=datetime.utcnow(),
                published=False
            )
            db.session.add(outbox_entry)

        return order.to_dict()

# Background worker yang publish event dari outbox
def outbox_publisher():
    """Jalankan sebagai background process terpisah."""
    while True:
        with db.session.begin():
            unpublished = OutboxEvent.query\
                .filter_by(published=False)\
                .order_by(OutboxEvent.created_at)\
                .limit(100)\
                .all()

            for event in unpublished:
                try:
                    publish_to_queue(event.event_type, event.payload)
                    event.published = True
                    event.published_at = datetime.utcnow()
                except Exception as e:
                    logger.error(f"Failed to publish event {event.id}: {e}")
                    # Tidak set published=True → akan di-retry di iterasi berikutnya

        time.sleep(5)  # cek setiap 5 detik

Kapan Microservices, Kapan Monolith #

Decision framework:

  Pertanyaan yang perlu dijawab:

  1. Seberapa besar tim?
     < 10 engineer: monolith hampir selalu lebih baik
     10-30 engineer: modular monolith, evaluate kebutuhan
     30+ engineer: microservices mulai masuk akal

  2. Apakah ada scaling requirement yang berbeda per domain?
     Semua domain tumbuh proportional → monolith OK
     Search butuh 10x scale lebih dari checkout → microservices masuk akal

  3. Seberapa matang domain bisnis?
     Baru mulai, banyak uncertainty → monolith (mudah di-refactor)
     Domain stabil dan dipahami → microservices lebih aman

  4. Apakah tim sudah berpengalaman dengan distributed systems?
     Tidak → mulai dengan monolith, belajar distributed systems dulu
     Ya → microservices bisa dipertimbangkan

  5. Apakah ada kebutuhan deployment independen?
     Semua fitur di-deploy bersama → monolith OK
     Tim berbeda perlu release kapan saja → microservices

  Jawaban umum: mulai dengan modular monolith.
  Evolusi ke microservices berdasarkan kebutuhan nyata, bukan antisipasi.

Anti-Pattern yang Harus Dihindari #

✗ Anti-pattern 1: Distributed Monolith
  Service yang saling bergantung tightly coupled
  Mengubah Service A selalu butuh mengubah Service B dan C
  Deploy harus dilakukan bersamaan
  → Semua keburukan monolith + semua keburukan distributed systems
  ✓ Solusi: perbaiki boundary, kurangi coupling antar service

✗ Anti-pattern 2: Terlalu banyak service terlalu dini
  "Nano-services": setiap fungsi menjadi service sendiri
  Setiap request butuh 15 service call
  Overhead jaringan mendominasi latency
  ✓ Solusi: mulai dengan service yang lebih besar, pecah hanya ketika ada alasan jelas

✗ Anti-pattern 3: Shared database antar service
  User Service dan Order Service pakai database yang sama
  Tidak ada independensi — perubahan schema di satu tempat mempengaruhi yang lain
  ✓ Solusi: database per service, komunikasi via API atau event

✗ Anti-pattern 4: Synchronous chain yang panjang
  Request → A → B → C → D → E → response
  Latency akumulatif + failure di mana saja gagalkan semua
  ✓ Solusi: redesain ke async event, atau kurangi chain dengan menggabungkan service

✗ Anti-pattern 5: Tidak ada distributed tracing
  Bug di production, tidak tahu request melewati service mana dan gagal di mana
  Log per service tidak cukup untuk investigasi distributed bug
  ✓ Solusi: OpenTelemetry + Jaeger/Zipkin dari awal

✗ Anti-pattern 6: Mengabaikan eventual consistency
  Mengasumsikan data selalu konsisten seperti monolith
  Bug yang aneh karena state tidak sinkron antar service
  ✓ Solusi: design untuk eventual consistency, gunakan Outbox Pattern

Checklist Microservices #

ARSITEKTUR:
  □ Setiap service punya domain bisnis yang jelas dan cohesive
  □ Service bisa di-deploy independen tanpa koordinasi dengan service lain
  □ Setiap service punya database sendiri (tidak sharing)
  □ Service boundary sudah di-validate dengan event storming atau domain analysis

KOMUNIKASI:
  □ Sinkron (REST/gRPC) hanya untuk yang membutuhkan response langsung
  □ Asinkron (message queue) untuk operasi yang tidak butuh response langsung
  □ Circuit breaker ada di semua sinkron call ke service lain
  □ Timeout dikonfigurasi untuk semua external call
  □ Retry dengan exponential backoff untuk transient failure

RELIABILITY:
  □ Health check endpoint di setiap service (/health atau /ready)
  □ Graceful shutdown — menyelesaikan request yang sedang diproses
  □ Dead letter queue untuk pesan yang gagal diproses
  □ Outbox pattern untuk reliable event publishing

OBSERVABILITY:
  □ Distributed tracing dengan OpenTelemetry
  □ Correlation ID di-propagate ke semua service
  □ Structured logging dengan trace ID di setiap log entry
  □ Metrics per service (request rate, error rate, latency)
  □ Centralized log aggregation (ELK, Loki)

API GATEWAY:
  □ Single entry point untuk semua client
  □ Authentication/Authorization di gateway layer
  □ Rate limiting di gateway
  □ API versioning strategy yang jelas

DEPLOYMENT:
  □ Setiap service punya CI/CD pipeline sendiri
  □ Container image yang immutable (tag dengan git SHA)
  □ Blue-green atau canary deployment untuk zero-downtime
  □ Rollback yang cepat dan teruji

DATA CONSISTENCY:
  □ Eventual consistency dipahami dan di-design dengan baik
  □ Saga pattern untuk long-running transaction
  □ Idempotent consumer untuk message processing

Ringkasan #

  • Mulai dengan monolith, evolusi ke microservices — jangan mulai dengan microservices untuk project baru. Modular monolith lebih mudah di-develop, di-test, dan di-debug, serta lebih mudah di-refactor menjadi microservices ketika kebutuhan nyata muncul.
  • Boundary yang benar adalah fondasi segalanya — boundary yang salah menciptakan distributed monolith yang lebih buruk dari keduanya. Gunakan DDD Bounded Context sebagai panduan.
  • Database per service adalah wajib — sharing database menghilangkan independensi yang menjadi alasan utama microservices. Tanpa ini, semua service tetap tightly coupled.
  • Pilih komunikasi berdasarkan kebutuhan — sinkron untuk query yang butuh response langsung, asinkron untuk aksi yang bisa di-buffer. Jangan default ke sinkron untuk semua.
  • Circuit breaker mencegah cascade failure — tanpa circuit breaker, kegagalan satu service menyebar ke seluruh sistem. Ini adalah perbedaan antara partial outage dan total outage.
  • Distributed tracing adalah kebutuhan, bukan opsional — debugging microservices tanpa tracing seperti debugging blind. Pasang OpenTelemetry dari hari pertama.
  • Eventual consistency adalah realita, bukan bug — dengan database per service, tidak ada strong consistency antar service. Design sistem untuk bekerja dengan ini, bukan melawan.
  • Outbox Pattern mencegah dual-write problem — menulis ke database dan mempublish event harus dalam satu operasi yang atomic. Outbox pattern memastikan event tidak hilang jika terjadi failure.
  • Kompleksitas operasional jauh lebih tinggi — lebih banyak service berarti lebih banyak hal yang bisa gagal, lebih banyak yang perlu di-monitor, lebih banyak deployment yang perlu di-coordinate. Pastikan tim siap.
  • Microservices bukan tentang teknologi — tentang organisasi — Conway’s Law: arsitektur sistem mencerminkan struktur komunikasi organisasi. Microservices paling berhasil ketika setiap service dimiliki oleh tim yang dedicated.

← Sebelumnya: Serverless   Berikutnya: Micro Frontend →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact