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:#000Setiap 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:#000Coupling 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 Proses | Karakteristik | Trigger yang Tepat |
|---|---|---|
| Scheduled / Event-based | Berjalan terjadwal atau dipicu event | EventBridge |
| Webhook / HTTP sporadis | Traffic tidak stabil, burst tak terduga | API Gateway |
| Async background job | Bisa delay, tidak perlu real-time | SQS |
| Heavy compute jarang | Mahal tapi jarang dibutuhkan | Lambda standalone |
| Always-on core API | Traffic stabil, latency sensitif | ECS / 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:#000EventBridge 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:#000Dengan 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:#000Keunggulan 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:#000Eliminasi 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.
| Metrik | Sebelum | Sesudah |
|---|---|---|
| Instance size (core service) | Besar (karena menanggung semua beban) | Bisa lebih kecil |
| Biaya workload sesekali | Dibayar 24/7 | Dibayar per eksekusi |
| Over-provisioning buffer | Perlu margin besar | Tidak diperlukan |
| Biaya saat traffic rendah | Tetap tinggi | Turun 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:#000Built-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 Besar | Lambda-Oriented (1 repo = 1 service) | |
|---|---|---|
| Deployment risk | Satu deploy mempengaruhi semua | Deploy per service, risk terisolasi |
| Onboarding | Perlu pahami seluruh sistem | Cukup pahami satu service |
| Ownership | Ambigu, dibagi-bagi | Jelas per tim / per repo |
| Rollback | Rollback semua atau tidak sama sekali | Rollback 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:
| Aspek | Golang | Node.js | Python |
|---|---|---|---|
| Cold start | Sangat cepat (< 100ms) | Cepat | Sedang |
| Memory footprint | Rendah | Sedang | Sedang–tinggi |
| Binary size | Kecil (single binary) | Besar (node_modules) | Sedang |
| Concurrency | Native goroutine | Event loop | Thread-based |
| Biaya Lambda | Lebih murah (runtime lebih pendek) | Sedang | Sedang |
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:#000Cold 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-off | Dampak | Mitigasi |
|---|---|---|
| Observability kompleks | Debugging lebih sulit | CloudWatch + X-Ray dari awal |
| Cold start | Latency tambahan di eksekusi pertama | Golang meminimalkan, Provisioned Concurrency jika perlu |
| Vendor lock-in | Sulit migrasi cloud | Keputusan sadar, dokumentasikan dengan jelas |
| Local dev kompleks | Pengembangan lebih lambat awalnya | LocalStack, 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:#000Secara 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:#000Setup 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.