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.
// HTTP client dengan timeout dan retry.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// getUserProfile memanggil User Service, retry dengan
// exponential backoff jika gagal.
func getUserProfile(ctx context.Context, userID int) (map[string]interface{}, error) {
    client := &http.Client{Timeout: 5 * time.Second}

    var lastErr error
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet,
            fmt.Sprintf("http://user-service/users/%d", userID), nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("X-Request-ID", getRequestID()) // propagasi trace context

        resp, err := client.Do(req)
        if err == nil && resp.StatusCode < 500 {
            defer resp.Body.Close()
            var result map[string]interface{}
            if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
                return nil, err
            }
            return result, nil
        }
        if resp != nil {
            resp.Body.Close()
        }
        lastErr = err

        // Exponential backoff: 1s, 2s, 4s (maksimal 10s)
        wait := time.Duration(1<<uint(attempt)) * time.Second
        if wait > 10*time.Second {
            wait = 10 * time.Second
        }
        select {
        case <-time.After(wait):
        case <-ctx.Done():
            return nil, ctx.Err()
        }
    }
    return nil, fmt.Errorf("getUserProfile failed after retries: %w", lastErr)
}

// getInventory menambahkan circuit breaker sehingga service yang gagal
// tidak menyebarkan kegagalan ke seluruh sistem.
func getInventory(ctx context.Context, productID int) (map[string]interface{}, error) {
    cb := newCircuitBreaker(5, 30*time.Second) // pseudo circuit breaker

    if !cb.allow() {
        return nil, fmt.Errorf("circuit is OPEN")
    }

    client := &http.Client{Timeout: 3 * time.Second}
    resp, err := client.Get(fmt.Sprintf("http://inventory-service/products/%d/stock", productID))
    if err != nil {
        cb.failure()
        return nil, err
    }
    defer resp.Body.Close()

    cb.success()
    var result map[string]interface{}
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    return result, nil
}
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.

package main

import (
    "context"
    "encoding/json"
    "os"
)

// OrderCreatedEvent dipublish ketika sebuah order dibuat.
type OrderCreatedEvent struct {
    EventType   string  `json:"event_type"`
    OrderID     string  `json:"order_id"`
    UserID      int     `json:"user_id"`
    TotalAmount float64 `json:"total_amount"`
    Items       []Item  `json:"items"`
}

// PublishOrderCreated mempublish event ke queue — Order
// Service tidak perlu tahu siapa yang akan consume event ini.
func PublishOrderCreated(orderID string, userID int, total float64, items []Item) error {
    event := OrderCreatedEvent{
        EventType:   "order.created",
        OrderID:     orderID,
        UserID:      userID,
        TotalAmount: total,
        Items:       items,
    }

    payload, err := json.Marshal(event)
    if err != nil {
        return err
    }

    // Panggilan SQS gaya aws-sdk-go-v2:
    _, err = sqsClient.SendMessage(context.Background(), &sqsv2.SendMessageInput{
        QueueUrl:    aws.String(os.Getenv("ORDER_EVENTS_QUEUE_URL")),
        MessageBody: aws.String(string(payload)),
    })
    return err
}

// HandleOrderCreated — Inventory Service mengonsumsi event order.created
// event. Jika Inventory Service down, pesan tetap di queue
// sampai diproses.
func HandleOrderCreated(eventData map[string]interface{}) {
    orderID := eventData["order_id"].(string)
    items := eventData["items"].([]interface{})

    for _, item := range items {
        m := item.(map[string]interface{})
        reserveInventory(m["product_id"].(string), m["quantity"].(int))
    }

    logger.Info("Inventory reserved for order %s", orderID)
}
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.

package main

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/trace"
)

// setupTracing mengonfigurasi tracer — dilakukan sekali saat startup.
func setupTracing(serviceName string) error {
    exporter, err := otlptracegrpc.New(context.Background(),
        otlptracegrpc.WithEndpoint("jaeger:4317"))
    if err != nil {
        return err
    }

    provider := trace.NewTracerProvider(
        trace.WithBatcher(exporter),
    )
    otel.SetTracerProvider(provider)
    return nil
}

// tracer untuk instrumentasi manual.
var tracer = otel.Tracer("orders")

// processOrder mencatat span untuk setiap langkah alur.
// Semua child span terhubung ke parent span yang ada.
func processOrder(ctx context.Context, orderData map[string]interface{}) error {
    ctx, span := tracer.Start(ctx, "process_order")
    defer span.End()
    span.SetAttributes(
        attribute.String("order.id", orderData["id"].(string)),
        attribute.Float64("order.total", orderData["total"].(float64)),
    )

    ctx, child := tracer.Start(ctx, "validate_payment")
    if err := validatePayment(ctx, orderData); err != nil {
        child.RecordError(err)
        child.End()
        return err
    }
    child.End()

    ctx, child = tracer.Start(ctx, "reserve_inventory")
    if err := reserveInventory(ctx, orderData["items"]); err != nil {
        child.RecordError(err)
        child.End()
        return err
    }
    child.End()

    _, child = tracer.Start(ctx, "notify_fulfillment")
    if err := publishOrderCreated(ctx, orderData); err != nil {
        child.RecordError(err)
        child.End()
        return err
    }
    child.End()

    return nil
}
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.

package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "time"
)

// OrderRepository membuat order dan mencatat event
// dalam database transaction yang sama.
type OrderRepository struct {
    db *sql.DB
}

// CreateOrderWithEvent membuat order DAN mencatat event
// dalam satu database transaction — tidak ada kemungkinan order dibuat
// tapi event tidak terpublish.
func (r *OrderRepository) CreateOrderWithEvent(orderData map[string]interface{}) (int64, error) {
    tx, err := r.db.Begin()
    if err != nil {
        return 0, err
    }
    defer tx.Rollback() // no-op setelah Commit

    // 1. Buat order
    result, err := tx.Exec(
        `INSERT INTO orders (user_id, total) VALUES (?, ?)`,
        orderData["user_id"], orderData["total"])
    if err != nil {
        return 0, err
    }
    orderID, err := result.LastInsertId() // generate order.id
    if err != nil {
        return 0, err
    }

    // 2. Simpan event ke outbox table dalam transaction yang sama
    payload, err := json.Marshal(map[string]interface{}{
        "order_id": orderID,
        "user_id":  orderData["user_id"],
        "total":    orderData["total"],
    })
    if err != nil {
        return 0, err
    }
    if _, err := tx.Exec(
        `INSERT INTO outbox (event_type, aggregate_id, payload, published)
         VALUES ('order.created', ?, ?, false)`,
        orderID, payload); err != nil {
        return 0, err
    }

    if err := tx.Commit(); err != nil {
        return 0, err
    }
    return orderID, nil
}

// OutboxPublisher berjalan sebagai background process terpisah,
// mempublish event dari outbox table.
func (r *OrderRepository) OutboxPublisher() {
    for {
        rows, err := r.db.Query(
            `SELECT id, event_type, payload FROM outbox
             WHERE published = false ORDER BY created_at LIMIT 100`)
        if err == nil {
            for rows.Next() {
                var (
                    id        int64
                    eventType string
                    payload   string
                )
                if err := rows.Scan(&id, &eventType, &payload); err != nil {
                    continue
                }
                if err := publishToQueue(eventType, payload); err == nil {
                    r.db.Exec(
                        `UPDATE outbox SET published = true, published_at = ? WHERE id = ?`,
                        time.Now(), id)
                } else {
                    log.Printf("Failed to publish event %d: %v", id, err)
                    // published tetap false → akan di-retry di iterasi berikutnya
                }
            }
            rows.Close()
        }

        time.Sleep(5 * time.Second) // 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