Microservices Architecture — Membangun Sistem Terdistribusi yang Scalable

Microservices Architecture — Membangun Sistem Terdistribusi yang Scalable

3/11/2025 Architecture By Tech Writers
MicroservicesSistem TerdistribusiArsitekturDevOpsKubernetesAPI DesignScalabilityCloud Native

Pengenalan: Kebangkitan Sistem Terdistribusi

Arsitektur microservices merepresentasikan pergeseran fundamental dari aplikasi monolitik ke sistem terdistribusi yang terdiri dari service kecil dan independen. Setiap microservice menangani kemampuan bisnis spesifik dan berkomunikasi dengan yang lain melalui API yang terdefinisi dengan baik. Pendekatan ini memungkinkan organisasi untuk menskalakan secara independen, melakukan deployment lebih cepat, dan mengorganisir tim di sekitar domain bisnis.

Panduan komprehensif ini mencakup prinsip-prinsip microservices, pola-pola arsitektur, strategi komunikasi, dan tantangan operasional yang perlu Kamu pahami sebelum mengadopsi arsitektur ini.

Daftar Isi

Monolith vs Microservices

Memahami perbedaan antara arsitektur monolitik dan microservices membantu Kamu membuat keputusan yang tepat tentang pendekatan mana yang sesuai dengan proyek dan organisasi Kamu.

Monolithic Architecture

Monolith adalah basis kode yang seragam dan terpadu yang di-deploy sebagai satu unit. Semua logika bisnis, akses database, dan antarmuka pengguna tightly coupled.

Karakteristik:

  • Basis kode dan unit deployment tunggal
  • Database bersama untuk semua fitur
  • Panggilan fungsi langsung antar modul
  • Pengembangan awal lebih sederhana
  • Komponen tightly coupled

Contoh Monolith Structure:

user-management/
├── routes/
├── controllers/
├── services/
├── models/
└── database.js

product-catalog/
├── routes/
├── controllers/
├── services/
├── models/
└── database.js

order-processing/
├── routes/
├── controllers/
├── services/
└── models/

Microservices Architecture

Microservices menguraikan aplikasi menjadi beberapa service independen, masing-masing dengan basis kode sendiri, database, dan unit deployment.

Karakteristik:

  • Basis kode dan deployment independen
  • Database terpisah per service
  • Komunikasi via REST API atau message queue
  • Kompleksitas operasional lebih tinggi
  • Service loosely coupled dan dapat diskalakan secara independen

Contoh Microservices Structure:

services/
├── user-service/
│   ├── src/
│   ├── database/
│   └── Dockerfile
├── product-service/
│   ├── src/
│   ├── database/
│   └── Dockerfile
├── order-service/
│   ├── src/
│   ├── database/
│   └── Dockerfile
└── payment-service/
    ├── src/
    ├── database/
    └── Dockerfile

Tabel Perbandingan

AspekMonolithMicroservices
DeploymentSingle unitIndependent service
ScalingSkalakan seluruh aplikasiSkalakan individual service
TeknologiSingle tech stackBerbagai stack per service
PengembanganLebih cepat di awalLebih banyak investasi awal
Dampak KegagalanBug satu dapat crash seluruh appKegagalan terisolasi ke service
Manajemen DataDatabase tunggalDatabase ganda
KompleksitasSederhana dipahamiSistem terdistribusi yang kompleks
Organisasi TimTim basis kode bersamaTim berorientasi domain

Core Microservices Principles

Implementasi microservices yang sukses bergantung pada beberapa prinsip inti yang membimbing keputusan arsitektur dan desain.

Single Responsibility Principle

Setiap microservice harus menangani satu kemampuan bisnis. Prinsip ini memastikan service tetap terfokus dan mudah dirawat.

❌ Salah: Multi-responsibility service
UserService {
  createUser()
  updateUser()
  deleteUser()
  processPayment()  // Not related to users
  sendEmail()       // Not related to users
  generateReport()  // Not related to users
}

✅ Benar: Single-responsibility service
UserService {
  createUser()
  updateUser()
  deleteUser()
}

PaymentService {
  processPayment()
  refundPayment()
}

NotificationService {
  sendEmail()
  sendSMS()
  sendPushNotification()
}

Bounded Contexts dari Domain-Driven Design

Organisir service di sekitar domain bisnis, bukan layer teknis. Setiap service memiliki model data dan logika domain sendiri.

// Contoh: E-commerce Domain
const domains = {
  userManagement: {
    responsibility: 'User accounts, profiles, authentication',
    service: 'UserService',
    dataOwned: ['users', 'profiles', 'preferences']
  },
  productCatalog: {
    responsibility: 'Product information, inventory',
    service: 'ProductService',
    dataOwned: ['products', 'inventory', 'categories']
  },
  orderProcessing: {
    responsibility: 'Orders, order fulfillment',
    service: 'OrderService',
    dataOwned: ['orders', 'order_items', 'shipments']
  },
  payments: {
    responsibility: 'Payment processing, transactions',
    service: 'PaymentService',
    dataOwned: ['transactions', 'payment_methods']
  }
};

Loose Coupling dan High Cohesion

Service harus meminimalkan ketergantungan pada service lain, memungkinkan pengembangan dan deployment independen.

❌ Tightly Coupled
UserService -> ProductService -> OrderService -> PaymentService
(Sequential, dependent deployments, change ripple effects)

✅ Loosely Coupled
UserService --[async event]--> EventBus <--[async event]-- ProductService
              --[REST API]-->  OrderService <--[REST API]--> PaymentService
(Independent, parallel deployments, isolated changes)

Pola Komunikasi Service

Services harus berkomunikasi secara efisien dan dapat diandalkan. Dua pola utama ada: synchronous dan asynchronous.

Komunikasi Synchronous (REST/gRPC)

Direct API calls di mana klien menunggu respons. Lebih sederhana untuk diimplementasikan tetapi menciptakan temporal coupling.

Komunikasi REST API:

// OrderService memanggil UserService
const UserService = require('./user-service-client');

async function createOrder(userId, items) {
  try {
    // Panggilan synchronous - memblokir hingga response
    const user = await UserService.getUser(userId);
    
    if (!user) {
      throw new Error('User tidak ditemukan');
    }
    
    const order = {
      userId,
      items,
      total: calculateTotal(items),
      createdAt: new Date()
    };
    
    return await saveOrder(order);
  } catch (error) {
    console.error('Gagal membuat order:', error);
    throw error;
  }
}

// ProductService memanggil InventoryService
async function reserveInventory(items) {
  const reservations = await Promise.all(
    items.map(item => 
      InventoryService.reserve(item.productId, item.quantity)
    )
  );
  
  return reservations;
}

gRPC Komunikasi:

// payment.proto
service PaymentService {
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}

message PaymentRequest {
  string orderId = 1;
  double amount = 2;
  string currency = 3;
}

message PaymentResponse {
  string transactionId = 1;
  string status = 2;
}

Kelebihan:

  • Sederhana untuk diimplementasikan dan dipahami
  • Model request-response synchronous
  • Mudah debugging dan tracing

Kekurangan:

  • Temporal coupling antara services
  • Cascading failures
  • Network latency berdampak
  • Sulit di-scale di bawah high load

Komunikasi Asynchronous (Message Queues)

Services berkomunikasi melalui message broker, decoupling producer dan consumer dalam waktu dan ruang.

Arsitektur Event-Driven:

// OrderService menerbitkan event
class OrderService {
  async createOrder(orderData) {
    const order = await saveOrder(orderData);
    
    // Terbitkan event ke message broker
    await messageQueue.publish('order.created', {
      orderId: order.id,
      userId: order.userId,
      items: order.items,
      timestamp: new Date()
    });
    
    return order;
  }
}

// Beberapa services subscribe ke events secara independent
class NotificationService {
  subscribe() {
    messageQueue.on('order.created', async (event) => {
      await sendOrderConfirmationEmail(event.userId);
    });
  }
}

class InventoryService {
  subscribe() {
    messageQueue.on('order.created', async (event) => {
      await reserveInventory(event.items);
    });
  }
}

class AnalyticsService {
  subscribe() {
    messageQueue.on('order.created', async (event) => {
      await logOrderMetrics(event);
    });
  }
}

Contoh RabbitMQ:

const amqp = require('amqplib');

async function publishEvent(exchange, routingKey, message) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();
  
  await channel.assertExchange(exchange, 'topic', { durable: true });
  
  channel.publish(
    exchange,
    routingKey,
    Buffer.from(JSON.stringify(message)),
    { persistent: true }
  );
  
  await channel.close();
  await connection.close();
}

async function subscribeToEvent(exchange, queue, routingKey, handler) {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();
  
  await channel.assertExchange(exchange, 'topic', { durable: true });
  await channel.assertQueue(queue, { durable: true });
  await channel.bindQueue(queue, exchange, routingKey);
  
  channel.consume(queue, async (msg) => {
    if (msg) {
      const content = JSON.parse(msg.content.toString());
      await handler(content);
      channel.ack(msg);
    }
  });
}

// Terbitkan order created event
await publishEvent('orders', 'order.created', {
  orderId: '12345',
  userId: 'user-1',
  amount: 99.99
});

// Subscribe ke order events
await subscribeToEvent('orders', 'email-queue', 'order.*', async (event) => {
  console.log('Order event diterima:', event);
  await sendEmail(event);
});

Kelebihan:

  • Loose coupling dalam waktu
  • Eventual consistency
  • Skalabilitas lebih baik
  • Services dapat gagal secara independent

Kekurangan:

  • Kompleksitas eventual consistency
  • Lebih sulit di-debug
  • Tantangan message ordering
  • Kompleksitas operasional

Pola API Gateway

API Gateway melayani sebagai single entry point untuk semua client requests, menangani routing, authentication, rate limiting, dan protocol translation.

Tanggung Jawab Gateway

// Implementasi API Gateway
const express = require('express');
const app = express();

// Pipeline middleware
app.use(authenticationMiddleware);
app.use(rateLimitMiddleware);
app.use(loggingMiddleware);

// Route ke services
app.get('/api/users/:id', async (req, res) => {
  const user = await userServiceClient.getUser(req.params.id);
  res.json(user);
});

app.get('/api/products', async (req, res) => {
  const products = await productServiceClient.listProducts(req.query);
  res.json(products);
});

app.post('/api/orders', async (req, res) => {
  const order = await orderServiceClient.createOrder(req.body);
  res.json(order);
});

// Middleware authentication
async function authenticationMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  try {
    const user = await verifyToken(token);
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Tidak terotorisasi' });
  }
}

// Middleware rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 menit
  max: 100 // limit masing-masing IP ke 100 requests per windowMs
});
app.use(limiter);

// Middleware logging
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

Contoh Kong API Gateway

# kong-deployment.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: kong-config
data:
  kong.conf: |
    database = postgres
    proxy_listen = 0.0.0.0:8000
    admin_listen = 0.0.0.0:8001

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kong
spec:
  replicas: 2
  selector:
    matchLabels:
      app: kong
  template:
    metadata:
      labels:
        app: kong
    spec:
      containers:
      - name: kong
        image: kong:latest
        ports:
        - containerPort: 8000
        - containerPort: 8001
        env:
        - name: KONG_DATABASE
          value: postgres
        - name: KONG_PG_HOST
          value: postgres

Service Discovery

Dalam dynamic environment seperti Kubernetes, services muncul dan hilang. Service discovery secara otomatis mendaftar dan menemukan services.

Pola Service Discovery

Client-Side Discovery:

// Client menemukan lokasi service
const ServiceRegistry = require('consul');
const consul = new ServiceRegistry();

async function callUserService(userId) {
  // Temukan lokasi service
  const services = await consul.health.service({
    service: 'user-service',
    passing: true
  });
  
  const service = services[Math.floor(Math.random() * services.length)];
  const url = `http://${service.Service.Address}:${service.Service.Port}`;
  
  // Panggil service
  const response = await fetch(`${url}/api/users/${userId}`);
  return response.json();
}

Server-Side Discovery (Kubernetes):

# Service DNS discovery di Kubernetes
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 3000
    targetPort: 3000
// Services otomatis dapat ditemukan oleh DNS
const userServiceUrl = 'http://user-service:3000';
const response = await fetch(`${userServiceUrl}/api/users/${userId}`);

Manajemen Data di Microservices

Tidak seperti monolith dengan database tunggal, microservices memerlukan strategi manajemen data yang berbeda.

Pola Database per Service

Setiap service memiliki database sendiri, memastikan loose coupling dan scaling independen.

UserService
├── users table
├── profiles table
└── PostgreSQL DB

ProductService
├── products table
├── inventory table
└── PostgreSQL DB

OrderService
├── orders table
├── order_items table
└── MongoDB

PaymentService
├── transactions table
├── payment_methods table
└── PostgreSQL DB

Implementasi:

// Setiap service terhubung ke database sendiri
const userDb = new Database({
  host: 'user-db.default.svc.cluster.local',
  database: 'users',
  user: 'user-service'
});

const productDb = new Database({
  host: 'product-db.default.svc.cluster.local',
  database: 'products',
  user: 'product-service'
});

// Tidak ada cross-database joins
async function getOrderDetails(orderId) {
  // Dapatkan order dari order service
  const order = await orderDb.query(
    'SELECT * FROM orders WHERE id = ?', 
    [orderId]
  );
  
  // Panggil product service API untuk mendapatkan product details
  const product = await fetch(
    `http://product-service:3000/api/products/${order.productId}`
  ).then(r => r.json());
  
  return { order, product };
}

Distributed Transactions dan Saga Pattern

Implementasikan transactions di seluruh services menggunakan Saga pattern, yang memecah distributed transactions menjadi local transactions.

// Saga pattern untuk order processing
class OrderSaga {
  async executeOrderSaga(orderData) {
    const sagaId = generateId();
    
    try {
      // Step 1: Create order
      const order = await this.orderService.createOrder(orderData);
      
      // Step 2: Reserve inventory
      await this.inventoryService.reserveInventory(order.items);
      
      // Step 3: Process payment
      const payment = await this.paymentService.processPayment(
        order.userId,
        order.total
      );
      
      // Step 4: Update order status
      await this.orderService.updateOrderStatus(order.id, 'CONFIRMED');
      
      return order;
      
    } catch (error) {
      // Compensating transaction - batalkan perubahan
      await this.compensate(order);
      throw error;
    }
  }
  
  async compensate(order) {
    // Reverse steps dalam urutan berlawanan
    try {
      // Release inventory
      await this.inventoryService.releaseInventory(order.items);
      
      // Refund payment
      await this.paymentService.refund(order.paymentId);
      
      // Cancel order
      await this.orderService.cancelOrder(order.id);
    } catch (error) {
      console.error('Compensation failed:', error);
      // Log untuk manual intervention
      await this.logCompensationFailure(order, error);
    }
  }
}

Pola Resilience

Membangun microservices yang resilient berarti menangani failures dengan graceful dan mencegah cascading failures.

Circuit Breaker Pattern

Mencegah cascading failures dengan menghentikan calls ke services yang gagal:

class CircuitBreaker {
  constructor(service, threshold = 5, timeout = 60000) {
    this.service = service;
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }
  
  async call(method, ...args) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }
    
    try {
      const result = await this.service[method](...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

// Usage
const breaker = new CircuitBreaker(userService);

try {
  const user = await breaker.call('getUser', userId);
} catch (error) {
  console.error('Service unavailable:', error);
}

Retry Pattern

Retry failed requests dengan exponential backoff:

async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 100) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      
      const delay = baseDelay * Math.pow(2, attempt);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Usage
const user = await retryWithBackoff(
  () => userService.getUser(userId),
  3,
  100
);

Timeout Pattern

Prevent requests from hanging indefinitely:

async function withTimeout(promise, timeoutMs = 5000) {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), timeoutMs)
    )
  ]);
}

// Usage
try {
  const user = await withTimeout(
    userService.getUser(userId),
    5000
  );
} catch (error) {
  if (error.message === 'Timeout') {
    console.error('Request timeout');
  }
}

Distributed Tracing

Lacak requests di seluruh multiple services untuk memahami behavior dan debug issues:

// Menggunakan OpenTelemetry
const { NodeTracerProvider } = require('@opentelemetry/node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { SimpleSpanProcessor } = require('@opentelemetry/tracing');

const provider = new NodeTracerProvider();
const exporter = new JaegerExporter({
  endpoint: 'http://localhost:14268/api/traces'
});

provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

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

async function createOrder(orderData) {
  const span = tracer.startSpan('createOrder');
  
  try {
    const order = await saveOrder(orderData);
    span.addEvent('order_created', { orderId: order.id });
    
    // Child span untuk inventory reservation
    const inventorySpan = tracer.startSpan('reserveInventory', { 
      parent: span 
    });
    await reserveInventory(order.items);
    inventorySpan.end();
    
    return order;
  } finally {
    span.end();
  }
}

Deployment dan Orchestration

Microservices memerlukan sophisticated orchestration platform seperti Kubernetes:

# Kubernetes deployment untuk UserService
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: myregistry/user-service:1.0.0
        ports:
        - containerPort: 3000
        env:
        - name: DB_HOST
          value: postgres
        - name: DB_PORT
          value: "5432"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 10

---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 3000
    targetPort: 3000
  type: ClusterIP

Monitoring dan Observability

Comprehensive monitoring adalah essential untuk managing complex microservices:

// Prometheus metrics
const prometheus = require('prom-client');

const requestCounter = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status']
});

const responseTime = new prometheus.Histogram({
  name: 'http_request_duration_ms',
  help: 'HTTP request duration dalam ms',
  labelNames: ['method', 'route'],
  buckets: [0.1, 5, 15, 50, 100, 500]
});

app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    requestCounter.labels(req.method, req.path, res.statusCode).inc();
    responseTime.labels(req.method, req.path).observe(duration);
  });
  
  next();
});

Kapan JANGAN Gunakan Microservices

Microservices tidak cocok untuk setiap project. Memahami kapan menghindarinya mencegah unnecessary complexity.

Tanda-tanda Kamu Belum Siap untuk Microservices

  1. Small Team: Jika Kamu tidak memiliki multiple teams yang dapat own services secara independen
  2. Simple Application: Monolith lebih sederhana untuk straightforward, single-domain applications
  3. Tight Deadlines: Microservices menambah complexity dan memperlambat initial development
  4. Limited DevOps: Memerlukan sophisticated deployment dan monitoring infrastructure
  5. Distributed System Inexperience: Tim Kamu harus memahami distributed systems
  6. Infrequent Scaling Needs: Monolith scales fine untuk banyak applications

Alternatif yang Lebih Baik:

  • Mulai dengan modular monolith - organized internal structure, single deployment
  • Gunakan monolith dengan separation of concerns - siapkan untuk migrate later jika diperlukan
  • Pertimbangkan serverless architecture untuk specific workloads

Kesimpulan: Microservices sebagai Keputusan Organisasi

Arsitektur microservices adalah keputusan organisasi seperti yang technical. Ini memungkinkan tim independent, deployment lebih cepat, dan flexible technology choice. Namun, ini menuntut sophisticated operational infrastructure, excellent monitoring, dan deep understanding tentang distributed system.

Mulai dengan monolith, pahami scalability needs Kamu, dan migrate ke microservices ketika complexity justified operational overhead. Ingat: microservices adalah optimization untuk organizational scaling, tidak selalu untuk technical performance.

Last Updated: January 8, 2026