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 ReportBatchItemFailures dan 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 →

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