Microservices Architecture — Membangun Sistem Terdistribusi yang Scalable
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
- Core Microservices Principles
- Service Communication Patterns
- API Gateway Pattern
- Service Discovery
- Data Management in Microservices
- Resilience Patterns
- Distributed Tracing
- Deployment and Orchestration
- Monitoring and Observability
- When NOT to Use Microservices
- Conclusion
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
| Aspek | Monolith | Microservices |
|---|---|---|
| Deployment | Single unit | Independent service |
| Scaling | Skalakan seluruh aplikasi | Skalakan individual service |
| Teknologi | Single tech stack | Berbagai stack per service |
| Pengembangan | Lebih cepat di awal | Lebih banyak investasi awal |
| Dampak Kegagalan | Bug satu dapat crash seluruh app | Kegagalan terisolasi ke service |
| Manajemen Data | Database tunggal | Database ganda |
| Kompleksitas | Sederhana dipahami | Sistem terdistribusi yang kompleks |
| Organisasi Tim | Tim basis kode bersama | Tim 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
- Small Team: Jika Kamu tidak memiliki multiple teams yang dapat own services secara independen
- Simple Application: Monolith lebih sederhana untuk straightforward, single-domain applications
- Tight Deadlines: Microservices menambah complexity dan memperlambat initial development
- Limited DevOps: Memerlukan sophisticated deployment dan monitoring infrastructure
- Distributed System Inexperience: Tim Kamu harus memahami distributed systems
- 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