Serverless Architecture #
Pada tahun 2014, AWS meluncurkan Lambda dengan janji yang terdengar hampir utopis bagi developer: tulis fungsi, upload, bayar hanya saat berjalan. Tidak ada server yang harus diprovisioning, tidak ada kapasitas yang harus diperkirakan, tidak ada patch OS yang harus dilakukan tengah malam. Realitanya lebih bernuansa — serverless tidak berarti tidak ada server, hanya berarti server bukan tanggung jawabmu. Serverless Architecture adalah model eksekusi di mana platform cloud mengelola seluruh infrastruktur: menjalankan fungsimu saat ada event, mengalokasikan resource sesuai kebutuhan, lalu mematikan semuanya saat tidak ada aktivitas. Kamu membayar per milidetik eksekusi, bukan per jam server. Untuk use case yang tepat — webhook handler, image processing, scheduled job, event consumer dengan traffic tidak merata — ini bisa sangat cost-effective dan sangat cepat untuk dikembangkan. Untuk use case yang salah, ini bisa menjadi mimpi buruk dari sisi latensi, observability, dan vendor lock-in.
Model Eksekusi Serverless #
flowchart TD
subgraph TRADITIONAL["Server Tradisional — Selalu Hidup"]
S["Server\n24/7 running\nbayar terus"]
S -->|"handle"| R1["Request 1"]
S -->|"handle"| R2["Request 2"]
S -->|"idle"| IDLE["... tidak ada request\ntetap bayar"]
end
subgraph FN["Serverless Function — On-Demand"]
E1["Event: HTTP Request"] -->|"trigger"| F1["Function Instance\nberjalan ~200ms"]
E2["Event: Queue Message"] -->|"trigger"| F2["Function Instance\nberjalan ~500ms"]
E3["Event: Cron"] -->|"trigger"| F3["Function Instance\nberjalan ~100ms"]
F1 & F2 & F3 -->|"selesai"| OFF["Instances dihentikan\ntidak ada biaya"]
end
Karakteristik fundamental serverless yang membedakannya dari arsitektur lain:
| Karakteristik | Penjelasan |
|---|---|
| Stateless | Setiap invokasi dimulai dari state bersih — tidak ada shared memory antar invokasi |
| Ephemeral | Container/runtime bisa dihancurkan kapan saja setelah eksekusi selesai |
| Event-driven | Fungsi hanya berjalan ketika ada trigger — HTTP, queue, cron, storage event |
| Auto-scaling | Platform otomatis menambah instance saat traffic naik, mengurangi saat turun |
| Pay-per-execution | Biaya berbanding lurus dengan penggunaan, bukan kapasitas yang disediakan |
Cold Start: Realita yang Perlu Dipahami #
Cold start adalah salah satu trade-off terbesar serverless. Ketika tidak ada instance yang “warm”, platform harus membuat container baru, menginisialisasi runtime, dan menjalankan kode inisialisasi sebelum request bisa diproses.
sequenceDiagram
participant C as Client
participant P as Platform
participant F as Function
Note over C,F: Cold Start — tidak ada instance yang warm
C->>P: Request masuk
P->>P: Alokasi container (50-200ms)
P->>P: Inisialisasi runtime (50-500ms)
P->>F: Load function code (10-100ms)
F->>F: Init code (DB conn, config) (50-500ms)
F->>F: Handle request (10-200ms)
F-->>C: Response (total: 200ms - 1500ms!)
Note over C,F: Warm Start — instance sudah ada
C->>P: Request berikutnya
P->>F: Route ke existing instance
F->>F: Handle request (10-200ms)
F-->>C: Response (total: 10-200ms)
Strategi mitigasi cold start:
// ✓ Inisialisasi mahal di luar handler — dijalankan sekali saat cold start
// Saat warm start, init sudah selesai
package main
import (
"context"
"database/sql"
"encoding/json"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
_ "github.com/lib/pq"
)
// Variabel global diinisialisasi sekali saat cold start
// ✓ Koneksi database di-reuse antar warm invocations
var (
db *sql.DB
config *AppConfig
)
// init() atau init di level package — berjalan saat container di-init
func init() {
var err error
// Load config
config = loadConfig()
// Buka koneksi database — mahal, tapi hanya sekali
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic("gagal koneksi database: " + err.Error())
}
// ✓ Set connection pool untuk reuse di warm start
db.SetMaxOpenConns(5) // serverless: jangan terlalu banyak koneksi
db.SetMaxIdleConns(2)
}
// Handler adalah entry point fungsi — diinvokasi setiap request
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// ✓ db dan config sudah siap — tidak perlu init ulang
userID := req.PathParameters["id"]
if userID == "" {
return badRequest("user ID wajib diisi"), nil
}
var user struct {
ID string `json:"id"`
FullName string `json:"full_name"`
Email string `json:"email"`
}
err := db.QueryRowContext(ctx,
`SELECT id, full_name, email FROM users WHERE id = $1`, userID,
).Scan(&user.ID, &user.FullName, &user.Email)
if err == sql.ErrNoRows {
return notFound("user tidak ditemukan"), nil
}
if err != nil {
return serverError("gagal ambil data user"), nil
}
body, _ := json.Marshal(user)
return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{"Content-Type": "application/json"},
Body: string(body),
}, nil
}
func main() {
lambda.Start(Handler)
}
func badRequest(msg string) events.APIGatewayProxyResponse {
return events.APIGatewayProxyResponse{
StatusCode: 400,
Body: `{"error":"` + msg + `"}`,
}
}
func notFound(msg string) events.APIGatewayProxyResponse {
return events.APIGatewayProxyResponse{
StatusCode: 404,
Body: `{"error":"` + msg + `"}`,
}
}
func serverError(msg string) events.APIGatewayProxyResponse {
return events.APIGatewayProxyResponse{
StatusCode: 500,
Body: `{"error":"` + msg + `"}`,
}
}
Tips mengurangi cold start di Go:
Mitigasi Cold Start:
✓ Go sudah sangat cepat untuk cold start dibanding Node.js atau Python
✓ Inisialisasi koneksi DB dan config di luar handler (level package)
✓ Minimalkan dependency eksternal — setiap import menambah binary size
✓ Provisioned Concurrency (AWS) — bayar untuk instance yang selalu warm
✓ Ping function secara berkala untuk menjaga tetap warm (CloudWatch Events)
✓ Gunakan connection pooler seperti PgBouncer antara Lambda dan PostgreSQL
Pola Umum Serverless #
Pola 1: HTTP API Gateway + Lambda #
// functions/create_order/main.go — function handler untuk POST /orders
package main
import (
"context"
"encoding/json"
"errors"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type CreateOrderRequest struct {
CustomerID string `json:"customer_id"`
Items []OrderItem `json:"items"`
}
type OrderItem struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
PriceCents int64 `json:"price_cents"`
}
type CreateOrderResponse struct {
OrderID string `json:"order_id"`
TotalCents int64 `json:"total_cents"`
}
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
var body CreateOrderRequest
if err := json.Unmarshal([]byte(req.Body), &body); err != nil {
return badRequest("request body tidak valid"), nil
}
// Validasi
if body.CustomerID == "" {
return badRequest("customer_id wajib diisi"), nil
}
if len(body.Items) == 0 {
return badRequest("items tidak boleh kosong"), nil
}
// Proses order
orderID, total, err := createOrder(ctx, body)
if err != nil {
if errors.Is(err, ErrCustomerNotFound) {
return notFound("customer tidak ditemukan"), nil
}
return serverError("gagal membuat order"), nil
}
respBody, _ := json.Marshal(CreateOrderResponse{
OrderID: orderID,
TotalCents: total,
})
return events.APIGatewayProxyResponse{
StatusCode: 201,
Headers: map[string]string{"Content-Type": "application/json"},
Body: string(respBody),
}, nil
}
func main() {
lambda.Start(Handler)
}
Pola 2: SQS Consumer #
// functions/process_payment/main.go — function handler untuk SQS messages
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type PaymentTask struct {
OrderID string `json:"order_id"`
AmountCents int64 `json:"amount_cents"`
Method string `json:"method"`
}
// Handler dipanggil dengan batch pesan dari SQS
func Handler(ctx context.Context, event events.SQSEvent) (events.SQSEventResponse, error) {
var failures []events.SQSBatchItemFailure
for _, msg := range event.Records {
if err := processMessage(ctx, msg); err != nil {
slog.ErrorContext(ctx, "gagal proses pesan",
"message_id", msg.MessageId,
"error", err,
)
// ✓ Laporkan sebagai failure — SQS akan retry message ini
failures = append(failures, events.SQSBatchItemFailure{
ItemIdentifier: msg.MessageId,
})
}
}
// ✓ Partial batch failure — hanya message yang gagal yang di-retry
return events.SQSEventResponse{BatchItemFailures: failures}, nil
}
func processMessage(ctx context.Context, msg events.SQSMessage) error {
var task PaymentTask
if err := json.Unmarshal([]byte(msg.Body), &task); err != nil {
// ✗ Pesan tidak valid — jangan retry, kirim ke DLQ
slog.Warn("pesan SQS tidak valid, skip", "message_id", msg.MessageId)
return nil // return nil agar tidak di-retry
}
slog.InfoContext(ctx, "memproses pembayaran",
"order_id", task.OrderID,
"amount_cents", task.AmountCents,
)
return processPayment(ctx, task)
}
func main() {
lambda.Start(Handler)
}
Pola 3: Scheduled Job (Cron) #
// functions/cleanup_expired_sessions/main.go — CloudWatch Events trigger
package main
import (
"context"
"database/sql"
"log/slog"
"time"
"github.com/aws/aws-lambda-go/lambda"
)
var db *sql.DB
func init() {
// Init DB connection
}
type ScheduledEvent struct {
Source string `json:"source"`
DetailType string `json:"detail-type"`
}
func Handler(ctx context.Context, event ScheduledEvent) error {
slog.InfoContext(ctx, "memulai cleanup expired sessions")
cutoff := time.Now().Add(-24 * time.Hour)
result, err := db.ExecContext(ctx,
`DELETE FROM sessions WHERE expires_at < $1`, cutoff,
)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
slog.InfoContext(ctx, "cleanup selesai", "deleted_count", rows)
return nil
}
func main() {
lambda.Start(Handler)
}
State Management di Serverless #
Karena fungsi stateless dan ephemeral, semua state harus disimpan di external store:
flowchart LR
subgraph FN["Serverless Function\n(stateless)"]
H["Handler"]
end
subgraph EXT["External State Stores"]
DB[(Database\nRDS / DynamoDB)]
CACHE[(Cache\nElastiCache / Redis)]
S3[(Object Storage\nS3 / GCS)]
SQ["Queue\nSQS / Pub/Sub"]
end
H -->|"read/write data"| DB
H -->|"session / temp data"| CACHE
H -->|"files / blobs"| S3
H -->|"async tasks"| SQ
// Contoh: upload file + simpan metadata (stateless, semua di external store)
func HandleFileUpload(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Decode file dari request body (base64)
fileData, err := base64.StdEncoding.DecodeString(req.Body)
if err != nil {
return badRequest("file tidak valid"), nil
}
// Simpan ke S3 — external storage
fileName := generateFileName()
if err := uploadToS3(ctx, fileName, fileData); err != nil {
return serverError("gagal upload file"), nil
}
// Simpan metadata ke DynamoDB — external state
metadata := FileMetadata{
FileID: fileName,
UserID: req.RequestContext.Authorizer["userId"].(string),
Size: len(fileData),
CreatedAt: time.Now().Unix(),
}
if err := saveMetadata(ctx, metadata); err != nil {
return serverError("gagal simpan metadata"), nil
}
// Kirim event untuk processing asinkron (resize, virus scan, dll)
if err := sendToSQS(ctx, ProcessFileTask{FileID: fileName}); err != nil {
slog.Warn("gagal kirim ke SQS", "file_id", fileName)
}
body, _ := json.Marshal(map[string]string{"file_id": fileName})
return events.APIGatewayProxyResponse{StatusCode: 201, Body: string(body)}, nil
}
Menghindari Vendor Lock-in #
Salah satu kekhawatiran terbesar serverless adalah vendor lock-in — kode yang bergantung langsung ke AWS SDK atau GCP SDK sulit dipindahkan ke platform lain.
// ✗ ANTI-PATTERN: business logic langsung bergantung ke AWS SDK
package main
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// ✗ Business logic di dalam handler yang tightly coupled ke AWS
client := dynamodb.NewFromConfig(awsConfig)
item, err := client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String("users"),
Key: map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: req.PathParameters["id"]},
},
})
// ... process item
}
// ✓ BENAR: Pisahkan business logic dari adapter platform
// Adapter mudah diganti tanpa menyentuh business logic
// core/user_service.go — business logic murni, tidak ada AWS dependency
package core
type UserRepository interface {
GetByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
if id == "" {
return nil, errors.New("ID tidak boleh kosong")
}
return s.repo.GetByID(ctx, id)
}
// adapter/dynamo/user_repo.go — AWS-specific adapter
package dynamo
type DynamoUserRepo struct {
client *dynamodb.Client
table string
}
func (r *DynamoUserRepo) GetByID(ctx context.Context, id string) (*core.User, error) {
// AWS DynamoDB specific code di sini
}
// handler/lambda/user_handler.go — AWS Lambda adapter tipis
package lambda_handler
import (
"github.com/aws/aws-lambda-go/events"
)
var svc *core.UserService // diinisialisasi di init()
func GetUserHandler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
id := req.PathParameters["id"]
user, err := svc.GetUser(ctx, id) // ✓ panggil business logic
// ... format response
}
Dengan pemisahan ini, mengganti DynamoDB ke PostgreSQL hanya membutuhkan ganti adapter — business logic tidak berubah. Mengganti AWS Lambda ke Google Cloud Functions hanya membutuhkan ganti handler wrapper.
Struktur Direktori Serverless Go #
myapp/
├── core/ ← Business logic — bebas dari cloud SDK
│ ├── user/
│ │ ├── service.go ← UserService dengan interface dependency
│ │ └── repository.go ← UserRepository interface
│ └── order/
│ └── service.go
│
├── adapter/ ← Implementasi untuk teknologi spesifik
│ ├── dynamo/
│ │ └── user_repo.go ← DynamoDB implementation
│ ├── postgres/
│ │ └── user_repo.go ← PostgreSQL implementation
│ └── s3/
│ └── file_storage.go ← S3 implementation
│
├── functions/ ← Entry point setiap fungsi Lambda
│ ├── get_user/
│ │ └── main.go ← AWS Lambda handler + wiring
│ ├── create_order/
│ │ └── main.go
│ ├── process_payment/
│ │ └── main.go
│ └── cleanup_sessions/
│ └── main.go
│
├── template.yaml ← AWS SAM template (infra as code)
└── Makefile ← Build commands per function
Infrastructure as Code dengan AWS SAM #
# template.yaml — AWS SAM template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: provided.al2023 # Go on AWS Lambda ARM
Architectures: [arm64]
Timeout: 30
MemorySize: 256
Environment:
Variables:
DATABASE_URL: !Sub '{{resolve:ssm:/myapp/${Env}/database_url}}'
Tracing: Active # ✓ X-Ray tracing otomatis
Resources:
# GET /users/{id}
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/get_user/
Handler: bootstrap
Events:
GetUser:
Type: Api
Properties:
Path: /users/{id}
Method: GET
# POST /orders
CreateOrderFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/create_order/
Handler: bootstrap
Events:
CreateOrder:
Type: Api
Properties:
Path: /orders
Method: POST
# SQS Consumer
ProcessPaymentFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/process_payment/
Handler: bootstrap
Events:
SQSQueue:
Type: SQS
Properties:
Queue: !GetAtt PaymentQueue.Arn
BatchSize: 10
FunctionResponseTypes:
- ReportBatchItemFailures # ✓ partial batch failure
PaymentQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 90
RedrivePolicy:
deadLetterTargetArn: !GetAtt PaymentDLQ.Arn
maxReceiveCount: 3
PaymentDLQ:
Type: AWS::SQS::Queue
# Scheduled cleanup setiap hari jam 2 pagi
CleanupFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/cleanup_sessions/
Handler: bootstrap
Events:
DailyCleanup:
Type: Schedule
Properties:
Schedule: cron(0 2 * * ? *)
Anti-Pattern yang Harus Dihindari #
// ✗ Menyimpan state di variabel yang dimodifikasi antar invokasi
var requestCount int // ✗ variabel ini mungkin di-share antar invocation
// atau di-reset saat cold start — tidak reliable
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (...) {
requestCount++ // ✗ state ini tidak konsisten antar invocations
}
// ✓ State yang perlu persisten harus disimpan ke external store
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (...) {
// ✓ update counter di DynamoDB atau Redis — konsisten dan scalable
incrementRequestCount(ctx, "my-api")
}
// ✗ Koneksi database dibuat ulang setiap invokasi
func Handler(ctx context.Context, ...) (...) {
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // ✗ mahal!
defer db.Close()
// ...
}
// ✓ Koneksi database di-init sekali di package level
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) // ✓ sekali
}
// ✗ Fungsi yang terlalu besar — melakukan terlalu banyak hal
func HandleEverything(ctx context.Context, req events.APIGatewayProxyRequest) (...) {
// ✗ handle semua endpoint dalam satu function
switch req.Path {
case "/users":
handleUsers(ctx, req)
case "/orders":
handleOrders(ctx, req)
case "/payments":
handlePayments(ctx, req)
}
}
// ✓ Satu fungsi = satu tanggung jawab
// get_user/main.go hanya handle GET /users/{id}
// create_order/main.go hanya handle POST /orders
// ✗ Long-running process di Lambda
func Handler(ctx context.Context, ...) (...) {
// ✗ proses yang butuh 20 menit — akan timeout di Lambda (max 15 menit)
processLargeDataset(ctx) // loop jutaan record
}
// ✓ Pecah ke job kecil via SQS
func Handler(ctx context.Context, ...) (...) {
// ✓ kirim setiap batch ke SQS untuk diproses secara paralel
batches := splitDatasetIntoBatches(dataset, 1000)
for _, batch := range batches {
sendToSQS(ctx, ProcessBatchTask{Records: batch})
}
}
Kapan Serverless, Kapan Tidak #
Serverless sangat cocok untuk:
✓ Traffic sangat tidak merata (spike tinggi tapi idle panjang)
✓ Background job yang tidak selalu berjalan (cron, webhook, event consumer)
✓ Startup atau MVP yang ingin zero ops overhead di awal
✓ Fungsi yang benar-benar stateless dan independen
✓ Event processing: image resize, email sender, notification dispatch
Hindari Serverless untuk:
✗ Long-running process (>15 menit) — gunakan container atau VM
✗ Aplikasi yang butuh latency sangat rendah dan konsisten — cold start tidak acceptable
✗ Sistem dengan banyak state internal yang kompleks
✗ Database-heavy operation yang butuh connection pool besar
✗ Tim yang belum familiar dengan distributed & event-driven paradigm
✗ Sistem yang butuh kontrol penuh atas runtime dan environment
Checklist Review Serverless Architecture #
FUNCTION DESIGN:
□ Satu fungsi = satu tanggung jawab yang jelas
□ Handler tipis — business logic di package terpisah (core/)
□ Function timeout dikonfigurasi sesuai ekspektasi maksimum
□ Memory size dikalibrasi berdasarkan profiling, bukan tebakan
STATELESS:
□ Tidak ada mutable global state yang dimodifikasi antar invokasi
□ Semua state tersimpan di external store (DB, cache, queue, storage)
□ Koneksi database menggunakan connection pooler di luar Lambda
COLD START:
□ Inisialisasi mahal (DB, config) dilakukan di level package, bukan dalam handler
□ Dependency yang tidak diperlukan tidak diimport
□ Pertimbangkan Provisioned Concurrency untuk endpoint dengan SLA ketat
ERROR HANDLING:
□ SQS consumer menggunakan ReportBatchItemFailures untuk partial failure
□ Dead Letter Queue dikonfigurasi untuk semua queue
□ Error di-categorize: permanent (jangan retry) vs transient (retry)
□ Timeout ditangani dengan graceful shutdown
SECURITY:
□ Setiap fungsi hanya punya IAM permission yang diperlukan (least privilege)
□ Secret dan konfigurasi disimpan di Parameter Store atau Secret Manager, bukan env var plaintext
□ Input dari API Gateway divalidasi di awal handler
OBSERVABILITY:
□ Structured logging dengan correlation ID
□ Distributed tracing (X-Ray, Cloud Trace) diaktifkan
□ Custom metrics untuk business events
□ Cold start frequency dimonitor
INFRASTRUCTURE AS CODE:
□ Semua resource didefinisikan dalam IaC (SAM, Terraform, CDK)
□ Tidak ada resource yang dibuat manual melalui console
□ Deployment pipeline menggunakan IaC, bukan manual upload
Ringkasan #
- Serverless bukan berarti tanpa server — server dikelola sepenuhnya oleh platform cloud; kamu hanya fokus pada kode dan logika bisnis.
- Cold start adalah trade-off nyata — inisialisasi mahal (DB, config) harus dilakukan di level package, bukan dalam handler; Go sudah lebih cepat dari Node.js dan Python untuk cold start.
- Stateless adalah keharusan — setiap state harus disimpan di external store; jangan pernah mengandalkan variabel lokal untuk menyimpan state antar invokasi.
- Satu fungsi = satu tanggung jawab — fungsi yang terlalu besar sulit diuji, di-deploy, dan di-debug; pecah menjadi fungsi kecil yang spesifik.
- Pisahkan business logic dari adapter platform — gunakan interface untuk dependency seperti repository dan storage; adapter mudah diganti tanpa menyentuh business logic saat pindah platform.
- SQS partial batch failure — jangan fail seluruh batch karena satu message bermasalah; gunakan
ReportBatchItemFailuresdan kirim message bermasalah ke DLQ.- Infrastructure as Code adalah wajib — deploy manual via console tidak sustainable; gunakan SAM, Terraform, atau CDK sejak hari pertama.
- Vendor lock-in bisa dimitigasi — dengan memisahkan business logic dari cloud SDK, migrasi antar platform menjadi penggantian adapter, bukan rewrite total.
- Observability lebih penting dari arsitektur lain — distributed, ephemeral, dan event-driven membuat debugging sangat sulit tanpa structured logging, distributed tracing, dan metrics yang baik.
- Bukan solusi universal — serverless sangat kuat untuk event-driven dan traffic tidak merata, tapi buruk untuk long-running process, latency sensitif, atau sistem stateful yang kompleks.
← Sebelumnya: CQRS Architecture Berikutnya: MVC Architecture →