Arsitektur Event-Driven dengan Claude Code: panduan praktis
Rancang event-driven system dengan Claude Code: kontrak, idempotency, retry, DLQ, observability, dan pitfalls.
Arsitektur event-driven sering dipromosikan sebagai cara membuat sistem lebih longgar dan mudah dikembangkan. Masalahnya, tanpa disiplin, ia juga bisa menyembunyikan ketergantungan dan membuat incident sulit dilacak. Jika Claude Code hanya diminta “buat jadi event-driven” tanpa nama event, kontrak payload, idempotency, retry, dead-letter queue, replay, dan batas logging, demo bisa berjalan tetapi operasi produksi rapuh.
Di artikel ini Claude Code dipakai sebagai reviewer dan asisten implementasi, bukan arsitek yang diterima tanpa kritik. Manusia tetap menentukan batas domain dan risiko bisnis. Claude Code membantu mengecek nama event, kompatibilitas schema, duplicate delivery, asumsi ordering, retry, DLQ, replay, dan observability. Contohnya mencakup signup SaaS, Webhook pembayaran ke fulfillment, audit log stream, dan pipeline notifikasi.
Dasar event-driven architecture
Event-driven architecture berarti satu service memublikasikan fakta yang sudah terjadi, lalu service lain bereaksi. Event bukan command. com.claudecodelab.user.created.v1 berarti “user sudah dibuat”, bukan “buat user”. Perbedaan ini menjaga producer agar tidak tahu detail kerja consumer.
Empat istilah cukup untuk mulai. Producer adalah pihak yang menerbitkan event. Consumer adalah pihak yang menerima dan memproses. Event bus atau queue adalah jalur pengiriman. Schema adalah kontrak payload, misalnya userId harus string dan email harus format email. Payload adalah data bisnisnya, sementara schema adalah batas yang harus dijaga.
Untuk referensi resmi, CloudEvents dan CloudEvents spec berguna sebagai format envelope event. Di AWS, Amazon EventBridge memberi contoh bus, filter, dan routing. Untuk observability, OpenTelemetry docs memberi struktur traces, metrics, dan logs.
Jangan mulai dengan meminta Claude Code mendesain semuanya dari nol. Berikan API yang ada, tabel database, Webhook, dan aturan recovery. Minta ia meninjau: apakah nama event jelas, apakah payload backward compatible, apakah duplicate delivery aman, apakah replay mungkin, dan apakah event bisa ditelusuri dari producer sampai consumer.
Kunci pertama: event contract
Kontrak harus ada sebelum handler. Tanpa kontrak, setiap consumer diam-diam bergantung pada field yang kebetulan dikirim producer hari ini. Perubahan kecil bisa merusak onboarding, billing, audit log, dan notification sekaligus.
YAML bergaya CloudEvents ini adalah template untuk event user-created pada SaaS. type berisi domain, fakta, dan versi. idempotencykey membuat duplicate delivery tidak menghasilkan side effect dua kali. correlationid menghubungkan logs dan traces dari request yang sama.
specversion: "1.0"
id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4"
type: "com.claudecodelab.user.created.v1"
source: "/services/identity"
subject: "users/usr_123"
time: "2026-06-02T09:30:00Z"
datacontenttype: "application/json"
dataschema: "https://example.com/schemas/user-created.v1.json"
idempotencykey: "user.created:usr_123:2026-06-02"
correlationid: "req_7fc42b"
data:
userId: "usr_123"
email: "masa@example.com"
plan: "starter"
locale: "id-ID"
Payload disimpan sebagai JSON Schema terpisah. Saat meminta Claude Code mengimplementasikan producer atau consumer, tulis jelas bahwa ia tidak boleh bergantung pada field di luar schema, tidak boleh menjadikan field opsional sebagai wajib tanpa versi baru, dan harus memisahkan breaking change ke v2.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-created.v1.json",
"title": "UserCreatedV1",
"type": "object",
"additionalProperties": false,
"required": ["userId", "email", "plan", "locale"],
"properties": {
"userId": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"plan": { "type": "string", "enum": ["free", "starter", "pro"] },
"locale": { "type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}$" }
}
}
Nama event sebaiknya berupa fakta dalam bentuk lampau. user.create dan sendEmail terdengar seperti perintah. user.created, payment.authorized, dan invoice.finalized adalah fakta. Hati-hati dengan user.updated; nama itu terlalu luas dan membuat consumer harus membaca payload untuk menebak apakah yang berubah email, plan, profil, atau timestamp login. Untuk perubahan penting, gunakan user.email_changed.v1 atau subscription.plan_changed.v1.
Gambar alur pengiriman
Sebelum implementasi, minta Claude Code membuat diagram Mermaid. Diagram membantu menemukan retry, DLQ, dan ketergantungan sinkron yang tersembunyi.
flowchart LR
A["Identity API<br/>producer"] --> B["Event bus<br/>filter and route"]
B --> C["Onboarding consumer<br/>workspace setup"]
B --> D["Email consumer<br/>welcome message"]
B --> E["Audit consumer<br/>append-only log"]
C --> F["Idempotency store"]
D --> F
C --> G["Dead-letter queue"]
D --> G
B --> H["OpenTelemetry<br/>traces metrics logs"]
Poin review utama: producer tidak menunggu semua consumer selesai. Jika API signup baru merespons setelah welcome email berhasil dikirim, sistem itu bukan benar-benar asynchronous. Itu hidden synchronous dependency. Jika memang dibutuhkan, tulis sebagai kontrak API sinkron. Jika tidak, desain pengalaman pengguna dan recovery untuk eventual consistency.
Consumer Node.js minimal
Kode berikut memproses user-created event, membuat onboarding workspace, memasukkan welcome email ke queue, mengabaikan duplicate event yang sama persis, dan menyimpan gagal akhir ke dead-letter queue. Contoh memakai Map agar mudah dibaca; production harus memakai Redis, DynamoDB, PostgreSQL, atau store bersama lain.
const crypto = require("node:crypto");
const processedEvents = new Map();
const deadLetterQueue = [];
function payloadHash(payload) {
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}
function eventKey(event) {
return event.idempotencykey || `${event.type}:${event.id}`;
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function withRetry(operation, options = {}) {
const attempts = options.attempts ?? 3;
const delayMs = options.delayMs ?? 250;
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === attempts) break;
await wait(delayMs * attempt);
}
}
throw lastError;
}
async function handleUserCreated(event, services) {
if (event.specversion !== "1.0") {
throw new Error(`Unsupported CloudEvents version: ${event.specversion}`);
}
if (event.type !== "com.claudecodelab.user.created.v1") {
throw new Error(`Unexpected event type: ${event.type}`);
}
const key = eventKey(event);
const currentHash = payloadHash(event.data);
const existing = processedEvents.get(key);
if (existing?.status === "succeeded" && existing.payloadHash === currentHash) {
return { status: "duplicate_ignored", key };
}
if (existing && existing.payloadHash !== currentHash) {
throw new Error("Idempotency key reused with a different payload");
}
processedEvents.set(key, {
status: "processing",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
});
try {
await withRetry(() => services.createOnboardingWorkspace(event.data.userId), {
attempts: 3,
delayMs: 200,
});
await withRetry(
() =>
services.enqueueWelcomeEmail({
userId: event.data.userId,
email: event.data.email,
correlationId: event.correlationid,
}),
{ attempts: 3, delayMs: 200 },
);
processedEvents.set(key, {
status: "succeeded",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
});
return { status: "processed", key };
} catch (error) {
processedEvents.set(key, {
status: "failed",
payloadHash: currentHash,
updatedAt: new Date().toISOString(),
errorMessage: error.message,
});
deadLetterQueue.push({
key,
event,
failedAt: new Date().toISOString(),
errorMessage: error.message,
});
throw error;
}
}
const services = {
async createOnboardingWorkspace(userId) {
console.log("workspace ready", { userId });
},
async enqueueWelcomeEmail(message) {
console.log("email queued", {
userId: message.userId,
correlationId: message.correlationId,
});
},
};
const exampleEvent = {
specversion: "1.0",
id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4",
type: "com.claudecodelab.user.created.v1",
source: "/services/identity",
time: "2026-06-02T09:30:00Z",
idempotencykey: "user.created:usr_123:2026-06-02",
correlationid: "req_7fc42b",
data: {
userId: "usr_123",
email: "masa@example.com",
plan: "starter",
locale: "id-ID",
},
};
handleUserCreated(exampleEvent, services)
.then((result) => console.log(result))
.catch((error) => console.error(error));
module.exports = { handleUserCreated, withRetry, deadLetterQueue };
Prompt untuk Claude Code harus spesifik: event yang sudah sukses jangan diproses ulang, idempotency key yang sama dengan payload berbeda harus ditolak, transient failure harus di-retry, dan failure final harus masuk DLQ. Prompt “tambahkan retry” terlalu umum dan bisa menghasilkan email atau entitlement ganda.
Empat use case praktis
| Use case | Events | Consumers | Risiko utama |
|---|---|---|---|
| Signup SaaS dan onboarding | user.created.v1, workspace.created.v1 | Settings, welcome email, CRM sync | API signup menunggu semua consumer |
| Payment Webhook ke fulfillment | payment.succeeded.v1, subscription.activated.v1 | Entitlements, invoice, Slack alert | Signature check atau idempotency terlewat |
| Audit log dan event stream | role.changed.v1, api_key.revoked.v1 | Append-only log, audit search, SIEM | PII tersimpan di long-retention logs |
| Notification pipeline | comment.mentioned.v1, report.ready.v1 | Email, in-app, push | Preference dan unsubscribe diabaikan |
Payment Webhook cocok untuk event-driven design, tetapi risiko bisnisnya tinggi. Untuk entry point, baca Webhook implementation with Claude Code. Untuk kontrak API, hubungkan dengan Production API development with Claude Code. Untuk migrasi event v1/v2, prinsip di API versioning with Claude Code sangat relevan.
Audit log butuh disiplin security. Jangan log full payload secara default. Gunakan Claude Code security audit dan Claude Code security best practices untuk menentukan field yang boleh disimpan lama. Bentuk error dan exception sebaiknya selaras dengan error handling patterns.
Pitfalls yang sering terjadi
Pertama, nama event terlalu vague. user.updated memaksa setiap consumer membaca payload dan membuat cabang logika sendiri.
Kedua, breaking payload change. Menghapus email, mengubah string ID menjadi object, atau membuat field opsional menjadi wajib bisa merusak consumer yang deploy terpisah. Field tambahan biasanya lebih aman; penghapusan, perubahan tipe, dan perubahan makna butuh versi baru.
Ketiga, duplicate delivery tidak ditangani. Banyak sistem event memakai at-least-once delivery: event hampir pasti sampai, tetapi bisa sampai lebih dari sekali. Email, payment, entitlement, dan points perlu idempotency key serta catatan processing yang durable.
Keempat, hidden synchronous dependency. Jika producer publish event lalu membaca table milik consumer sebelum merespons, coupling belum hilang.
Kelima, tidak ada replay plan. Jika bug consumer membuat tiga jam event gagal, tim harus tahu retention window, filter replay, perilaku duplicate, dan aturan suppress side effect.
Keenam, observability lemah. Logs harus punya event id, type, correlation id, consumer name, retry count, dan alasan DLQ. Metrics perlu backlog age, failure rate, duplicate count, dan replay count.
Ketujuh, logging PII. PII adalah data yang bisa mengidentifikasi orang, seperti email, nama, alamat, payment detail, dan token. Gunakan event id dan userId, mask field sensitif, dan tetapkan retention.
Template review Claude Code
Minta review sebelum meminta implementasi.
# Claude Code EDA review checklist
Scope:
- event contract: schemas/user-created.v1.json
- producer: services/identity
- consumers: onboarding, email, audit-log
Please review:
- Is the event name a past-tense fact?
- Is the payload change backward compatible for existing consumers?
- Is there an idempotency key, and does duplicate delivery avoid double side effects?
- Does any consumer call back into the producer synchronously?
- Are retry count, backoff, and dead-letter rules explicit?
- Can replay run without duplicate email, payment, or irreversible effects?
- Do logs avoid PII and secrets?
- Can OpenTelemetry show event id, correlation id, and consumer name?
Output:
- P0/P1/P2 risks
- Files that should change
- Tests that should be added
- Open decisions a human must make
Jika Claude Code menemukan asumsi berbahaya, perbaiki batasnya sebelum menulis kode. Setelah itu, minta perubahan schema, handler, test, dan runbook secara berurutan.
Runbook operasi
Event-driven system bernilai jika bisa dioperasikan saat failure. Letakkan runbook bersama consumer pertama.
# Runbook: event backlog or DLQ growth
## Symptoms
- Queue age is over 5 minutes
- Dead-letter queue has more than 10 messages
- Consumer error rate is over 2 percent for 10 minutes
## First checks
1. Identify event type, consumer name, and correlation id.
2. Check whether the failure is validation, downstream timeout, or permission.
3. Confirm whether the producer is still publishing new events.
4. Stop replay if the event triggers email, payment, or irreversible side effects.
## Recovery
1. Fix the consumer or downstream dependency.
2. Replay a small batch with idempotency enabled.
3. Compare processed count, duplicate count, and DLQ count.
4. Resume normal processing.
5. Write the incident note with event ids, time range, and customer impact.
## Never do
- Do not edit payloads manually without recording the reason.
- Do not replay payment or email events without suppression rules.
- Do not paste full payloads with PII into chat or issue trackers.
Sebelum merge, tanya Claude Code: “failure apa yang tidak bisa dipulihkan oleh runbook ini?” Pertanyaan itu sering membuka masalah permission, schema drift, atau asumsi terhadap API eksternal.
Ringkasan dan CTA
Event-driven architecture berhasil jika event contract diperlakukan seperti public API. Nama, schema, versioning, idempotency, ordering, retries, dead-letter handling, replay, dan observability harus diputuskan secara eksplisit. Claude Code paling berguna saat meninjau keputusan itu dan mengimplementasikan perubahan kecil berdasarkan kontrak yang jelas.
ClaudeCodeLab membantu training Claude Code, review desain event-driven, kontrak Webhook/API, strategi audit log, incident runbook, dan workflow tim. Jika tim Anda ingin membuat Webhook lebih aman, memindahkan notifikasi ke async workers, atau menstandarkan prompt review Claude Code, mulai dari Claude Code training and consulting. Untuk belajar mandiri, lihat free cheat sheet dan product templates.
Masa menguji alur ini pada prototipe SaaS kecil. Ketika event contract dan idempotency key ditulis lebih dulu, perubahan dari Claude Code lebih kecil dan mudah direview. Pada prototipe sebelumnya yang hanya memakai user.updated, consumer notifikasi dan audit mulai bercabang berdasarkan detail payload, sementara replay tidak jelas. Setelah nama event dipisah dan runbook DLQ ditambahkan, jelas event mana yang harus di-replay, dari rentang waktu apa, dan berapa record yang diharapkan.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission receipt Claude Code: mencatat scope, bukti, dan rollback
Pola permission receipt untuk Claude Code: aksi yang diizinkan, batas approval, command verifikasi, rollback, dan cek CTA revenue.
Agent Harness Aman untuk Claude Code dan Codex: Permission, Verifikasi, dan Rollback
Rancang Agent Harness praktis untuk Claude Code dan Codex dengan policy, plan, verification, dan recovery layer.
Subagent Claude Code: panduan praktis untuk delegasi artikel dan kode
Panduan subagent Claude Code untuk membagi pekerjaan artikel dan kode: aturan delegasi, prompt, risiko, dan checklist.