Strategi Menurunkan Cost dan Meningkatkan Resiliensi dengan AWS Event-Driven & Lambda-Oriented Architecture
11 min read

Strategi Menurunkan Cost dan Meningkatkan Resiliensi dengan AWS Event-Driven & Lambda-Oriented Architecture

Di banyak sistem yang sudah berjalan cukup lama, ada pola yang berulang: ada beberapa proses yang jarang berjalan, tapi harus selalu ada server yang standby untuk menjalankannya. Hasilnya? Resource teralokasi 24/7 untuk workload yang sebenarnya hanya aktif beberapa menit per hari. Ini bukan masalah teknis kecil — ini adalah pola yang terus menguras biaya operasional dan mempersulit scaling.

Artikel ini membahas strategi arsitektur yang konsisten dan berulang untuk menyelesaikan masalah ini: memindahkan proses yang tidak berjalan terus-menerus ke AWS event-driven dan Lambda-oriented architecture. Bukan sekadar optimasi teknis, tapi keputusan desain yang berdampak langsung ke cost, resiliensi sistem, dan kecepatan tim.

Gambaran Besar Arsitektur

Sebelum masuk ke detail alasan dan dampaknya, penting untuk memahami pola arsitekturnya terlebih dahulu. Intinya sederhana:

graph TB
    subgraph "Monolith (Core Business)"
        M["Service Utama<br/>Always-on, core API"]
    end

    subgraph "Lambda Service A — Scheduled Jobs"
        EB["EventBridge<br/>(Trigger)"] --> LA["Lambda<br/>Cron / Scheduled Task"]
    end

    subgraph "Lambda Service B — Webhooks"
        AG["API Gateway<br/>(Trigger)"] --> LB["Lambda<br/>Webhook Handler"]
    end

    subgraph "Lambda Service C — Async Processing"
        SQ["SQS Queue<br/>(Trigger)"] --> LC["Lambda<br/>Background Worker"]
    end

    subgraph "Infra Management"
        TF["Terraform<br/>1 repo = 1 service<br/>Infra + Code lifecycle"]
    end

    M -.->|"Emit events"| SQ
    M -.->|"Emit events"| EB
    ExtSystem["External System<br/>(Stripe, Shopee, dll)"] --> AG

    TF -.-|"Manages"| LA & LB & LC

    style M fill:#bfdbfe,stroke:#2563eb,color:#000
    style EB fill:#fef3c7,stroke:#d97706,color:#000
    style AG fill:#fef3c7,stroke:#d97706,color:#000
    style SQ fill:#fef3c7,stroke:#d97706,color:#000
    style LA fill:#bbf7d0,stroke:#16a34a,color:#000
    style LB fill:#bbf7d0,stroke:#16a34a,color:#000
    style LC fill:#bbf7d0,stroke:#16a34a,color:#000
    style TF fill:#f5f5f4,stroke:#78716c,color:#000
    style ExtSystem fill:#f5f5f4,stroke:#78716c,color:#000

Setiap Lambda service mengikuti satu pola yang konsisten: 1 repository = 1 logical service, dengan beberapa Lambda di dalamnya yang punya trigger berbeda. Satu service bisa punya satu Lambda untuk scheduled job, satu untuk webhook, dan satu untuk async processing — semuanya dalam satu repo, satu Terraform module, satu ownership tim.


Masalah yang Dipecahkan — Monolith dengan Beban Tidak Kontinu

Sebelum bicara solusi, perlu dipahami dulu masalah yang terjadi di lapangan.

Resource Selalu Aktif untuk Beban Sesekali

Banyak monolith harus selalu hidup dengan kapasitas tertentu karena ada proses berat yang jarang tapi tidak bisa ditinggal: cron job harian yang memakan CPU tinggi selama 10 menit, webhook dari payment gateway yang datang sporadis, atau background job yang hanya aktif saat ada event tertentu.

Akibatnya, server di-provision untuk peak capacity bukan average capacity — dan selisihnya adalah uang yang terus dibayarkan meski resource idle.

graph LR
    subgraph "❌ Monolith: Resource Idle Tapi Tetap Dibayar"
        Server["EC2 / ECS Instance<br/>Selalu aktif, selalu dibayar"]
        Cron["Cron Job<br/>Aktif 10 menit/hari"]
        Webhook["Webhook Handler<br/>Aktif saat ada request"]
        Worker["Async Worker<br/>Aktif saat ada pesan"]

        Server --> Cron & Webhook & Worker
        Idle["CPU idle 90% waktu<br/>Memory teralokasi 100% waktu"]
        Server --> Idle
    end

    style Server fill:#fecaca,stroke:#dc2626,color:#000
    style Idle fill:#fecaca,stroke:#dc2626,color:#000
    style Cron fill:#fef3c7,stroke:#d97706,color:#000
    style Webhook fill:#fef3c7,stroke:#d97706,color:#000
    style Worker fill:#fef3c7,stroke:#d97706,color:#000

Coupling yang Menjadi Failure Domain Bersama

Yang lebih berbahaya dari cost adalah failure domain yang tidak terisolasi. Ketika cron job berat berbagi database connection pool dengan API utama, dan worker async berbagi deployment cycle dengan core business logic — satu proses non-kritis bisa mengakibatkan degradasi di seluruh sistem.

Scaling Linear yang Tidak Efisien

Di monolith, scaling harus dilakukan secara keseluruhan. Menambah instance berarti semua logic ikut di-scale, meskipun yang sebenarnya membutuhkan scaling hanyalah satu proses. Ini adalah pemborosan struktural yang sulit diperbaiki dari dalam monolith.


Prinsip Desain — Bayar Hanya untuk yang Berjalan

Seluruh strategi ini bertumpu pada satu prinsip yang sederhana tapi berdampak besar:

Jika sebuah proses tidak berjalan terus-menerus, jangan bayarkan resource seolah-olah ia berjalan 24/7.

Dari sini, proses dalam sistem diklasifikasikan berdasarkan pola eksekusi, bukan hanya domain bisnis.

Jenis ProsesKarakteristikTrigger yang Tepat
Scheduled / Event-basedBerjalan terjadwal atau dipicu eventEventBridge
Webhook / HTTP sporadisTraffic tidak stabil, burst tak terdugaAPI Gateway
Async background jobBisa delay, tidak perlu real-timeSQS
Heavy compute jarangMahal tapi jarang dibutuhkanLambda standalone
Always-on core APITraffic stabil, latency sensitifECS / EC2 (tetap di monolith)

Klasifikasi ini penting: tidak semua proses cocok jadi Lambda. Proses yang traffic-nya stabil dan latency-nya sangat sensitif tetap lebih cocok di server tradisional. Lambda bukan pengganti semua beban — ia adalah tempat yang tepat untuk beban yang tidak kontinu.


Empat Pola Trigger yang Digunakan

EventBridge + Lambda — Scheduled dan Business Events

EventBridge adalah pengganti cron job server yang jauh lebih bersih. Tidak perlu server dedicated untuk menjaga schedule — EventBridge memicu Lambda tepat saat dibutuhkan, lalu Lambda mati setelah selesai.

graph LR
    EB1["EventBridge<br/>Schedule: setiap hari jam 02.00"]
    EB2["EventBridge<br/>Business event: OrderShipped"]
    L1["Lambda<br/>Daily Report Generator"]
    L2["Lambda<br/>Post-shipment Notifier"]

    EB1 --> L1
    EB2 --> L2

    style EB1 fill:#fef3c7,stroke:#d97706,color:#000
    style EB2 fill:#fef3c7,stroke:#d97706,color:#000
    style L1 fill:#bbf7d0,stroke:#16a34a,color:#000
    style L2 fill:#bbf7d0,stroke:#16a34a,color:#000

EventBridge juga cocok sebagai event bus antar service — service A emit event, service B dan C bereaksi secara independen tanpa perlu tahu satu sama lain. Ini adalah cara yang tepat untuk integrasi yang loose-coupled antar domain.

API Gateway + Lambda — Webhook dan HTTP Sporadis

Webhook dari payment gateway, shipping provider, atau third-party service hampir selalu punya pola yang sama: tidak bisa diprediksi kapan datangnya, tapi harus diproses dengan andal. Pola ini sangat mahal jika dihandle oleh server always-on.

graph LR
    Stripe["Stripe Webhook"]
    Shopee["Marketplace Webhook"]
    Internal["Internal HTTP<br/>Admin trigger"]

    AG["API Gateway"]

    L3["Lambda<br/>Payment Event Handler"]
    L4["Lambda<br/>Order Sync Handler"]
    L5["Lambda<br/>Admin Action Handler"]

    Stripe --> AG
    Shopee --> AG
    Internal --> AG

    AG --> L3 & L4 & L5

    style AG fill:#fef3c7,stroke:#d97706,color:#000
    style L3 fill:#bbf7d0,stroke:#16a34a,color:#000
    style L4 fill:#bbf7d0,stroke:#16a34a,color:#000
    style L5 fill:#bbf7d0,stroke:#16a34a,color:#000

Dengan API Gateway + Lambda: tidak ada server standby, cost hampir nol saat idle, dan traffic spike dari external system tidak merusak core service.

SQS + Lambda — Async Processing dengan Isolasi Penuh

SQS adalah trigger paling powerful untuk background processing karena menyediakan backpressure otomatis, retry bawaan, dan Dead Letter Queue (DLQ) tanpa effort tambahan.

graph LR
    Monolith["Core Service"] -->|"Enqueue"| SQS["SQS Queue"]
    SQS --> LC["Lambda Worker"]
    LC -->|"Success"| Done["Proses selesai"]
    LC -->|"Failure (3x retry)"| DLQ["Dead Letter Queue"]
    DLQ --> Alert["CloudWatch Alert<br/>Manual investigation"]

    style Monolith fill:#bfdbfe,stroke:#2563eb,color:#000
    style SQS fill:#fef3c7,stroke:#d97706,color:#000
    style LC fill:#bbf7d0,stroke:#16a34a,color:#000
    style DLQ fill:#fecaca,stroke:#dc2626,color:#000
    style Alert fill:#f5f5f4,stroke:#78716c,color:#000

Keunggulan pola ini: kegagalan worker tidak pernah mencapai end user. Jika Lambda gagal, pesan tetap di queue, di-retry sesuai policy, dan jika habis retry baru masuk DLQ untuk investigasi. Core service tidak tahu dan tidak peduli apakah worker sedang gagal.

Lambda Standalone — Heavy Compute yang Jarang

Untuk proses yang mahal secara komputasi tapi hanya dibutuhkan sesekali — transformasi data besar, generate laporan kompleks, atau integrasi eksternal yang berat — Lambda standalone adalah pilihan yang paling cost-efficient.


Dampak ke Cost — Lebih dari Sekadar Penghematan

Pay-Per-Use yang Sesungguhnya

Model billing Lambda adalah per request + per 100ms execution time. Tidak ada konsep “instance idle” — jika tidak ada eksekusi, tidak ada biaya.

graph LR
    subgraph "Model Biaya: Monolith vs Lambda"
        M_Cost["EC2 / ECS<br/>Biaya = Jam × Resource<br/>Tidak peduli ada traffic atau tidak"]
        L_Cost["Lambda<br/>Biaya = Jumlah eksekusi × Durasi<br/>Nol saat tidak ada eksekusi"]
    end

    M_Cost -->|"Traffic spike"| M_Peak["Biaya naik, perlu scale out"]
    M_Cost -->|"Traffic rendah"| M_Idle["Biaya tetap, resource idle"]

    L_Cost -->|"Traffic spike"| L_Peak["Biaya naik proporsional"]
    L_Cost -->|"Traffic rendah"| L_Idle["Biaya turun mendekati nol"]

    style M_Cost fill:#fecaca,stroke:#dc2626,color:#000
    style M_Peak fill:#fecaca,stroke:#dc2626,color:#000
    style M_Idle fill:#fecaca,stroke:#dc2626,color:#000
    style L_Cost fill:#bbf7d0,stroke:#16a34a,color:#000
    style L_Peak fill:#bbf7d0,stroke:#16a34a,color:#000
    style L_Idle fill:#bbf7d0,stroke:#16a34a,color:#000

Eliminasi Over-Provisioning

Di monolith, provisioning dilakukan untuk worst case — peak traffic, peak concurrent job, peak memory. Lambda menghapus kebutuhan ini karena scaling terjadi otomatis dan granular. Tidak perlu buffer capacity untuk “jaga-jaga.”

Monolith Menjadi Lebih Ramping

Dampak tidak langsung yang sering diabaikan: ketika proses non-kritis dipindahkan ke Lambda, monolith menjadi lebih kecil dan lebih fokus. Instance bisa di-downgrade, memory allocation berkurang, dan database connection pool tidak lagi terbebani oleh job background.

MetrikSebelumSesudah
Instance size (core service)Besar (karena menanggung semua beban)Bisa lebih kecil
Biaya workload sesekaliDibayar 24/7Dibayar per eksekusi
Over-provisioning bufferPerlu margin besarTidak diperlukan
Biaya saat traffic rendahTetap tinggiTurun signifikan

Dampak ke Resiliensi — Isolasi yang Sesungguhnya

Failure Domain yang Terpisah

Ini adalah dampak resiliensi paling signifikan. Ketika Lambda gagal — apapun penyebabnya — monolith dan service lain tidak tahu dan tidak terdampak.

graph TB
    subgraph "❌ Tanpa Isolasi: Failure Menyebar"
        Mono["Monolith<br/>(Core API + Cron + Worker + Webhook)"]
        Bug["Bug di cron job"] -->|"Shared DB, shared memory"| Mono
        Mono -->|"Core API ikut terdampak"| Down["Degradasi / Downtime"]
    end

    subgraph "✅ Dengan Isolasi: Failure Terbatas"
        CoreOK["Monolith<br/>(Core API saja)"]
        LambdaFail["Lambda Worker gagal"] -->|"SQS DLQ, retry isolated"| Recover["Pesan masuk DLQ<br/>Alert ke tim"]
        CoreOK -->|"Tidak terdampak"| OK["Core API tetap sehat"]
    end

    style Bug fill:#fecaca,stroke:#dc2626,color:#000
    style Down fill:#fecaca,stroke:#dc2626,color:#000
    style LambdaFail fill:#fef3c7,stroke:#d97706,color:#000
    style Recover fill:#fef3c7,stroke:#d97706,color:#000
    style CoreOK fill:#bbf7d0,stroke:#16a34a,color:#000
    style OK fill:#bbf7d0,stroke:#16a34a,color:#000

Built-in Retry dan DLQ

SQS dan EventBridge menyediakan retry otomatis dengan exponential backoff dan Dead Letter Queue tanpa perlu implementasi tambahan. Ini adalah fault tolerance yang biasanya membutuhkan effort besar jika dibangun sendiri di dalam monolith.

Auto-Scaling Tanpa Capacity Planning

Lambda scale otomatis berdasarkan concurrency — tidak perlu konfigurasi scaling policy, tidak perlu menentukan min/max instance, tidak perlu alarm untuk trigger scale-out. Untuk workload yang sifatnya burst (webhook, async job), ini sangat menguntungkan.


Dampak ke Tim — Ownership yang Lebih Jelas

Satu Repository, Satu Service, Satu Tim

Pola 1 repository = 1 logical service memberikan kepemilikan yang tidak ambigu. Tim tahu persis apa yang mereka kelola, apa yang bisa mereka ubah tanpa mempengaruhi tim lain, dan apa yang harus dikoordinasikan.

Monolith BesarLambda-Oriented (1 repo = 1 service)
Deployment riskSatu deploy mempengaruhi semuaDeploy per service, risk terisolasi
OnboardingPerlu pahami seluruh sistemCukup pahami satu service
OwnershipAmbigu, dibagi-bagiJelas per tim / per repo
RollbackRollback semua atau tidak sama sekaliRollback per service

Deployment Lebih Aman

Perubahan pada Lambda service tidak memerlukan deployment monolith. Ini mengurangi coordination overhead antar tim dan memungkinkan deployment yang lebih sering dengan risiko lebih kecil.

Golang sebagai Pilihan Bahasa

Penggunaan Golang untuk Lambda bukan preferensi, tapi keputusan teknis yang mendukung keputusan bisnis:

AspekGolangNode.jsPython
Cold startSangat cepat (< 100ms)CepatSedang
Memory footprintRendahSedangSedang–tinggi
Binary sizeKecil (single binary)Besar (node_modules)Sedang
ConcurrencyNative goroutineEvent loopThread-based
Biaya LambdaLebih murah (runtime lebih pendek)SedangSedang

Untuk Lambda yang billing-nya per 100ms, runtime yang lebih cepat dan memory yang lebih rendah secara langsung mengurangi biaya.


Trade-off — Yang Perlu Disadari Sejak Awal

Strategi ini bukan tanpa biaya. Ada trade-off nyata yang harus disadari sebelum commit ke pendekatan ini.

Kompleksitas Observability

Dengan banyak Lambda service kecil, distributed tracing dan centralized logging menjadi keharusan, bukan pilihan. Tanpa ini, debugging masalah lintas service menjadi sangat sulit.

graph LR
    subgraph "Minimum Observability Stack"
        CW["CloudWatch Logs<br/>(semua Lambda)"]
        XRay["AWS X-Ray<br/>(distributed tracing)"]
        Alarm["CloudWatch Alarms<br/>(DLQ depth, error rate)"]
        Dashboard["CloudWatch Dashboard<br/>(unified view)"]
    end

    Lambda1["Lambda A"] & Lambda2["Lambda B"] & Lambda3["Lambda C"] --> CW & XRay
    CW & XRay --> Dashboard
    CW --> Alarm

    style CW fill:#bfdbfe,stroke:#2563eb,color:#000
    style XRay fill:#bfdbfe,stroke:#2563eb,color:#000
    style Alarm fill:#fef3c7,stroke:#d97706,color:#000
    style Dashboard fill:#bbf7d0,stroke:#16a34a,color:#000

Cold Start

Lambda yang jarang dipanggil akan mengalami cold start — inisialisasi runtime sebelum eksekusi pertama setelah periode idle. Untuk Golang, cold start biasanya di bawah 100ms dan jarang menjadi masalah. Untuk runtime lain atau Lambda dengan dependency berat, ini bisa lebih terasa.

Mitigasi yang tersedia: Provisioned Concurrency untuk Lambda yang latency-sensitif, meski ini menghapus sebagian keuntungan cost dari pay-per-use.

Vendor Lock-in

Arsitektur ini sangat AWS-specific: EventBridge, SQS, API Gateway, Lambda, DLQ — semuanya AWS managed service. Berpindah ke cloud provider lain atau ke on-premise akan membutuhkan effort yang signifikan. Ini perlu disadari dan diterima sebagai keputusan sadar, bukan dihindari.

Local Development yang Lebih Kompleks

Event-driven architecture sulit disimulasikan secara lokal. Menjalankan alur lengkap EventBridge → Lambda → SQS → Lambda membutuhkan tooling tambahan (LocalStack, AWS SAM CLI, atau mocking layer). Tim perlu investasi waktu untuk setup lingkungan development yang cukup representatif.

Trade-offDampakMitigasi
Observability kompleksDebugging lebih sulitCloudWatch + X-Ray dari awal
Cold startLatency tambahan di eksekusi pertamaGolang meminimalkan, Provisioned Concurrency jika perlu
Vendor lock-inSulit migrasi cloudKeputusan sadar, dokumentasikan dengan jelas
Local dev kompleksPengembangan lebih lambat awalnyaLocalStack, SAM CLI, buat abstraksi testable

Kapan Strategi Ini Tidak Cocok

Ada kondisi di mana pendekatan ini justru menjadi beban. Penting untuk mengenali sinyal-sinyal ini.

graph TD
    Q1{"Apakah workload<br/>berjalan terus-menerus<br/>dengan traffic stabil?"}
    Q2{"Apakah latency<br/>sangat kritikal<br/>(sub-10ms)?"}
    Q3{"Apakah logic<br/>sangat stateful dan<br/>sulit di-event-driven?"}
    Q4{"Apakah tim punya<br/>kapasitas untuk manage<br/>observability terdistribusi?"}

    Q1 -->|"Ya"| NotFit1["Lambda kurang cocok<br/>ECS / EC2 lebih efisien"]
    Q1 -->|"Tidak"| Q2
    Q2 -->|"Ya"| NotFit2["Lambda kurang cocok<br/>Cold start tidak bisa diterima"]
    Q2 -->|"Tidak"| Q3
    Q3 -->|"Ya"| NotFit3["Perlu evaluasi lebih dalam<br/>Mungkin butuh redesain lebih dulu"]
    Q3 -->|"Tidak"| Q4
    Q4 -->|"Tidak"| NotFit4["Tunda, investasi observability dulu<br/>Sebelum perbanyak Lambda"]
    Q4 -->|"Ya"| Fit["Strategi ini cocok diterapkan"]

    style NotFit1 fill:#fecaca,stroke:#dc2626,color:#000
    style NotFit2 fill:#fecaca,stroke:#dc2626,color:#000
    style NotFit3 fill:#fef3c7,stroke:#d97706,color:#000
    style NotFit4 fill:#fef3c7,stroke:#d97706,color:#000
    style Fit fill:#bbf7d0,stroke:#16a34a,color:#000

Secara ringkas, strategi ini kurang ideal jika:

  • Traffic tinggi dan stabil 24/7 — server always-on lebih cost-efficient
  • Latency ultra-low sangat kritikal — cold start Lambda tidak bisa diterima
  • Logic sangat stateful dan sulit dipecah menjadi event-driven
  • Tim belum siap atau belum punya tooling untuk observability terdistribusi

Urutan Implementasi yang Disarankan

Jangan pindahkan semua sekaligus. Pendekatan evolusioner jauh lebih aman dan memungkinkan validasi di setiap tahap.

graph LR
    T1["Tahap 1<br/>Identifikasi proses<br/>tidak kontinu di monolith"]
    T2["Tahap 2<br/>Setup observability dulu<br/>CloudWatch + X-Ray"]
    T3["Tahap 3<br/>Pindahkan satu proses<br/>paling aman ke Lambda"]
    T4["Tahap 4<br/>Validasi: cost, reliability,<br/>observability cukup?"]
    T5["Tahap 5<br/>Ulangi untuk proses<br/>berikutnya bertahap"]

    T1 --> T2 --> T3 --> T4
    T4 -->|"Ya"| T5
    T4 -->|"Tidak"| Fix["Perbaiki dulu sebelum lanjut"]
    T5 --> T3

    style T1 fill:#f5f5f4,stroke:#78716c,color:#000
    style T2 fill:#bfdbfe,stroke:#2563eb,color:#000
    style T3 fill:#fef3c7,stroke:#d97706,color:#000
    style T4 fill:#fef3c7,stroke:#d97706,color:#000
    style T5 fill:#bbf7d0,stroke:#16a34a,color:#000
    style Fix fill:#fecaca,stroke:#dc2626,color:#000

Setup observability sebelum pindahkan proses pertama adalah langkah yang sering dilewati tapi sangat penting. Tanpa visibility ke Lambda execution, log, dan error rate — masalah yang muncul setelah migrasi akan sangat sulit di-debug.


Ringkasan

  • Prinsip inti: jika proses tidak berjalan terus-menerus, jangan bayarkan resource seolah ia berjalan 24/7. Lambda adalah tempat yang tepat untuk workload tidak kontinu — scheduled job, webhook, async processing, dan heavy compute jarang.
  • Empat pola trigger: EventBridge untuk scheduled dan business events, API Gateway untuk webhook/HTTP sporadis, SQS untuk async processing dengan retry/DLQ bawaan, Lambda standalone untuk heavy compute jarang.
  • Pola repo: 1 repository = 1 logical service, dengan beberapa Lambda di dalamnya yang punya trigger berbeda. Seluruh lifecycle dimanage dengan Terraform.
  • Cost: pay-per-use yang sesungguhnya — nol biaya saat idle, billing proporsional dengan aktivitas bisnis, dan monolith bisa lebih ramping karena tidak menanggung beban non-kritis.
  • Resiliensi: isolasi failure domain yang nyata — Lambda gagal tidak mempengaruhi core service. SQS memberikan retry dan DLQ tanpa implementasi tambahan.
  • Golang untuk Lambda: cold start sangat cepat, memory footprint rendah, runtime lebih pendek = biaya lebih murah.
  • Trade-off yang harus disadari: observability terdistribusi menjadi keharusan, cold start ada (minimal dengan Golang), vendor lock-in AWS, dan local development lebih kompleks.
  • Implementasi bertahap: mulai dari satu proses paling aman, setup observability dulu sebelum pindahkan proses pertama, validasi di setiap tahap sebelum lanjut.