Advanced (Diperbarui: 2/6/2026)

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 dengan Claude Code: panduan praktis

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 caseEventsConsumersRisiko utama
Signup SaaS dan onboardinguser.created.v1, workspace.created.v1Settings, welcome email, CRM syncAPI signup menunggu semua consumer
Payment Webhook ke fulfillmentpayment.succeeded.v1, subscription.activated.v1Entitlements, invoice, Slack alertSignature check atau idempotency terlewat
Audit log dan event streamrole.changed.v1, api_key.revoked.v1Append-only log, audit search, SIEMPII tersimpan di long-retention logs
Notification pipelinecomment.mentioned.v1, report.ready.v1Email, in-app, pushPreference 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.

#Claude Code #event-driven #architecture #CQRS #design patterns
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.