Advanced (Mis à jour: 02/06/2026)

Architecture événementielle avec Claude Code : guide pratique

Concevoir des événements avec Claude Code : contrats, idempotence, retries, DLQ, observabilité et pièges.

Architecture événementielle avec Claude Code : guide pratique

L’architecture événementielle promet des systèmes plus découplés, mais elle peut aussi cacher des dépendances et rendre les incidents opaques. Si l’on demande simplement à Claude Code de “rendre le système event-driven”, sans noms d’événements précis, contrat de payload, idempotence, retry, dead-letter queue et règles de logs, le premier prototype peut marcher tandis que la première panne devient difficile à comprendre.

Dans ce guide, Claude Code est utilisé comme relecteur et assistant d’implémentation, pas comme architecte à suivre aveuglément. Les décisions de domaine restent humaines. Claude Code sert à vérifier si les noms d’événements sont clairs, si le schema est compatible, si la livraison en double est sûre, si l’ordre est supposé sans preuve, si le replay est possible et si l’observabilité couvre le chemin complet. Les cas traités sont l’inscription SaaS, le Webhook de paiement vers le fulfillment, le flux d’audit et le pipeline de notifications.

Les bases à clarifier

Une architecture événementielle publie des faits déjà survenus. Un événement n’est pas une commande. com.claudecodelab.user.created.v1 signifie “un utilisateur a été créé”, et non “crée un utilisateur”. Cette nuance évite au producer de connaître les détails des consumers.

Quatre mots suffisent pour démarrer. Le producer émet l’événement. Le consumer le reçoit et le traite. L’event bus ou la queue transporte l’événement. Le schema est le contrat du payload, par exemple le fait que userId soit une chaîne non vide et que email soit une adresse valide. En donnant ces mots à Claude Code, on réduit ses suppositions.

Pour les références officielles, CloudEvents et la CloudEvents spec donnent un format d’enveloppe commun. Sur AWS, Amazon EventBridge illustre le bus, le filtrage et le routing. Pour l’observabilité, les OpenTelemetry docs structurent traces, metrics et logs.

La bonne demande à Claude Code n’est pas “invente l’architecture”. Donnez-lui plutôt l’API existante, les tables, les Webhooks et les contraintes d’incident, puis demandez-lui de critiquer le nommage, la compatibilité du payload, la sécurité face aux doublons, le replay et la traçabilité de bout en bout.

Verrouiller le contrat d’événement

Le contrat vient avant le code du handler. Sans contrat, chaque consumer dépend silencieusement de ce que le producer envoie aujourd’hui. Une petite modification peut casser l’onboarding, la facturation, l’audit et les notifications.

Ce YAML inspiré de CloudEvents sert de modèle pour un événement de création d’utilisateur SaaS. type encode le domaine, le fait et la version. idempotencykey permet de traiter une livraison dupliquée sans double effet. correlationid relie les logs et traces issus de la même requête.

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: "fr-FR"

Le payload doit être décrit séparément avec JSON Schema. Quand Claude Code génère le producer ou le consumer, précisez qu’il ne doit pas dépendre de champs hors schema, ni rendre obligatoire un champ optionnel sans nouvelle version.

{
  "$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}$" }
  }
}

Les noms doivent être des faits au passé. user.create ou sendEmail ressemblent à des commandes. user.created, payment.authorized et invoice.finalized décrivent des faits. Évitez user.updated dès que plusieurs changements importants existent. Sinon, chaque consumer devra lire le payload pour deviner s’il est concerné. Préférez user.email_changed.v1 ou subscription.plan_changed.v1.

Visualiser le flux

Avant d’implémenter, demandez à Claude Code un diagramme Mermaid. Cela rend visibles les retries, la DLQ et les dépendances synchrones cachées.

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"]

Le point clé est que le producer ne doit pas attendre tous les consumers. Si l’API d’inscription attend que l’e-mail de bienvenue soit envoyé pour répondre, l’architecture n’est pas vraiment asynchrone. C’est une dépendance synchrone masquée. Il faut soit l’assumer dans le contrat API, soit concevoir l’expérience autour d’une cohérence éventuelle.

Consumer Node.js minimal

Le code suivant traite l’événement de création d’utilisateur, crée l’état d’onboarding, place un e-mail de bienvenue en file, ignore un doublon exact et envoie les échecs vers une dead-letter queue. Il utilise Map pour rester lisible; en production, utilisez Redis, DynamoDB, PostgreSQL ou un autre store partagé.

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: "fr-FR",
  },
};

handleUserCreated(exampleEvent, services)
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

module.exports = { handleUserCreated, withRetry, deadLetterQueue };

Le prompt doit être précis: ne pas retraiter un événement déjà réussi, refuser une même idempotency key avec un payload différent, retenter les erreurs transitoires, conserver l’échec final en DLQ. “Ajoute un retry” est trop vague et peut produire des e-mails ou des droits en double.

Quatre cas d’usage

CasÉvénementsConsumersRisque principal
Inscription SaaS et onboardinguser.created.v1, workspace.created.v1Paramètres initiaux, e-mail, CRML’API attend tous les consumers
Webhook de paiement vers fulfillmentpayment.succeeded.v1, subscription.activated.v1Droits, facture, SlackSignature ou idempotence absente
Audit log et event streamrole.changed.v1, api_key.revoked.v1Log append-only, recherche, SIEMPII dans des logs longue durée
Pipeline de notificationscomment.mentioned.v1, report.ready.v1E-mail, in-app, pushPréférences et désabonnement ignorés

Les Webhooks de paiement sont un bon cas, mais ils demandent de la rigueur. Reliez ce guide à Webhook implementation with Claude Code. Pour les contrats API, voyez Production API development with Claude Code. Pour les migrations v1/v2, API versioning with Claude Code s’applique directement.

Pour l’audit, ne loggez pas le payload complet par défaut. Lisez Claude Code security audit et Claude Code security best practices pour décider ce qui peut être conservé. Les réponses d’erreur et exceptions doivent aussi suivre error handling patterns.

Pièges fréquents

Le premier piège est le nom vague. user.updated oblige chaque consumer à lire le payload et à deviner s’il est concerné.

Le deuxième est le changement cassant. Supprimer email, transformer un ID string en objet ou rendre obligatoire un champ optionnel peut casser des consumers déployés séparément. Les ajouts sont souvent plus sûrs; suppressions, types et sens changés exigent une nouvelle version.

Le troisième est l’oubli des doublons. Beaucoup de systèmes garantissent at-least-once delivery: l’événement arrive au moins une fois, mais parfois plusieurs. E-mail, paiement, droits et points exigent idempotency key et trace durable de traitement.

Le quatrième est la dépendance synchrone cachée. Si le producer publie un événement puis lit la base du consumer avant de répondre, rien n’est vraiment découplé.

Le cinquième est l’absence de replay plan. Après trois heures d’événements échoués, il faut connaître la rétention, le filtre de replay, le comportement face aux doublons et la suppression des effets irréversibles.

Le sixième est l’observabilité faible. Les logs doivent contenir event id, type, correlation id, consumer, retry count et raison de DLQ. Les métriques doivent couvrir backlog, taux d’échec, doublons et replay.

Le septième est le logging de PII. PII désigne les données permettant d’identifier une personne. Évitez e-mails, adresses, tokens et détails de paiement dans logs, tickets et chats.

Template de review Claude Code

Demandez une review avant l’implémentation.

# 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

Si Claude Code trouve une hypothèse dangereuse, corrigez la frontière avant le code. Ensuite seulement, demandez schema, handler, tests et runbook, dans cet ordre.

Runbook opérationnel

Un système événementiel doit être exploitable pendant une panne. Ajoutez ce runbook dès le premier consumer.

# 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.

Avant de merger, demandez à Claude Code: “quel échec ce runbook ne permet-il pas de récupérer ?” La réponse révèle souvent permissions manquantes, schema drift ou dépendances externes.

Conclusion et accompagnement

L’architecture événementielle fonctionne si le contrat d’événement est traité comme une API publique. Noms, schema, versioning, idempotency, ordering, retries, dead-letter handling, replay et observability doivent être explicites. Claude Code est utile quand il relit ces décisions et implémente des changements limités contre un contrat clair.

ClaudeCodeLab accompagne les équipes sur la formation Claude Code, la review d’architecture événementielle, les contrats Webhook/API, l’audit log, les runbooks d’incident et les workflows d’équipe. Si vous voulez sécuriser vos Webhooks, déplacer les notifications vers des workers asynchrones ou standardiser les prompts de review, commencez par Claude Code training and consulting. Les ressources en autonomie sont aussi disponibles via la free cheat sheet et les product templates.

Masa a testé ce flux dans un petit prototype SaaS. Quand le contrat d’événement et l’idempotency key étaient écrits d’abord, les changements produits par Claude Code étaient plus petits et plus simples à relire. Dans un prototype précédent basé seulement sur user.updated, les consumers de notification et d’audit ont commencé à brancher sur les détails du payload, et le replay restait flou. Séparer les noms d’événements et ajouter le runbook DLQ a rendu la récupération concrète: quels événements rejouer, depuis quelle plage horaire et avec quel volume attendu.

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

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.