DEV Community

Cover image for Event-Driven Architecture pentru platformă auto în RO
Alexandru Draghici
Alexandru Draghici

Posted on • Originally published at auto101.ro

Event-Driven Architecture pentru platformă auto în RO

Event-Driven Architecture: listări auto, dealeri și leasing

TL;DR

  • O event-driven architecture te ajută să decuplezi fluxuri precum listări, verificare dealeri, leasing și notificări.
  • Cheia e consistența: folosește Transactional Outbox, idempotency și un contract de eveniment versionat.
  • Observability (tracing + metrics) și o schemă (Avro/JSON Schema) reduc debug-ul în producție.

De ce o platformă auto ajunge rapid la complexitate

Construiești o platformă ca auto101.ro: listări pentru mașini noi și rulate, dealeri verificați, opțiuni de leasing, consultanță gratuită și „livrare la cheie”. Inițial pare CRUD: adaugi ofertă, o publici, o vezi în listă.

Apoi apar cerințele care îți rup monolitul:

  • publicare listare ⇒ indexare în search, calcul preț „corect”, audit, notificări
  • dealer verificat ⇒ badge, reguli de eligibilitate, scoring, blocare automată
  • cerere leasing ⇒ pre-calificare, documente, status updates, timeline pentru client
  • comparații între opțiuni ⇒ agregări, caching, invalidări

Când fiecare acțiune „scrie în 5 tabele și mai cheamă 3 servicii”, începi să simți de ce event-driven architecture e atractivă: fiecare domeniu reacționează la evenimente, nu la call-uri în lanț.

O soluție pragmatică: event-driven architecture cu un „spine” de evenimente

Nu ai nevoie de microservicii peste tot din ziua 1. Dar un backbone de evenimente îți permite să scalezi pe măsură ce crește produsul.

Domenii și bounded contexts

Un mod util de a împărți:

  • Listings: listare, preț, status, media
  • Dealers: KYC/validare, rating, comisioane
  • Leasing: lead, ofertare, status, documente
  • Search/Discovery: index, ranking, sugestii
  • Notifications: email/SMS/push, template-uri, preferințe

Într-o event-driven architecture, fiecare context publică evenimente „de fapt” (facts): ListingPublished, DealerVerified, LeasingLeadCreated.

Contractul de eveniment: schema, versiuni și idempotency

Un contract bun îți economisește luni de suport.

Recomandări de payload

  • include event_id (UUID) și occurred_at
  • include aggregate_id + aggregate_type
  • include schema_version
  • include trace_id pentru debugging distribuit

Exemplu JSON (simplificat):

{
  "event_id": "c7f3d5c6-2a8f-4f3b-9d4b-1c2a4a8c9f3e",
  "type": "ListingPublished",
  "schema_version": 2,
  "occurred_at": "2026-02-25T10:23:41.120Z",
  "aggregate_type": "listing",
  "aggregate_id": "listing_12345",
  "trace_id": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "data": {
    "dealer_id": "dealer_77",
    "price": 12990,
    "currency": "EUR",
    "car": {
      "make": "Volkswagen",
      "model": "Golf",
      "year": 2017,
      "fuel": "diesel"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Idempotency la consumatori

Orice consumer trebuie să fie pregătit pentru duplicate (retries, replays). Model simplu:

  • tabel processed_events(event_id, processed_at)
  • dacă event_id există, skip

Implementare: Transactional Outbox (fără „dual write”)

Greșeala clasică: scrii în DB și publici în broker separat. Dacă publicarea eșuează, ai date fără eveniment.

Transactional Outbox rezolvă: în aceeași tranzacție salvezi și schimbarea, și un record în outbox. Un worker publică ulterior.

Schema minimală

CREATE TABLE listings (
  id TEXT PRIMARY KEY,
  status TEXT NOT NULL,
  dealer_id TEXT NOT NULL,
  price_cents INT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE outbox (
  id UUID PRIMARY KEY,
  event_type TEXT NOT NULL,
  aggregate_id TEXT NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  published_at TIMESTAMPTZ NULL
);

CREATE INDEX outbox_unpublished_idx ON outbox (created_at)
WHERE published_at IS NULL;
Enter fullscreen mode Exit fullscreen mode

Publicare listare + outbox (pseudo TypeScript)

import { db } from "./db";
import { v4 as uuid } from "uuid";

export async function publishListing(listingId: string) {
  await db.tx(async (t) => {
    const listing = await t.one(
      `UPDATE listings
       SET status = 'PUBLISHED', updated_at = now()
       WHERE id = $1
       RETURNING id, dealer_id, price_cents`,
      [listingId]
    );

    const event = {
      event_id: uuid(),
      type: "ListingPublished",
      schema_version: 2,
      occurred_at: new Date().toISOString(),
      aggregate_type: "listing",
      aggregate_id: listing.id,
      data: {
        dealer_id: listing.dealer_id,
        price_cents: listing.price_cents
      }
    };

    await t.none(
      `INSERT INTO outbox(id, event_type, aggregate_id, payload)
       VALUES($1, $2, $3, $4::jsonb)`,
      [event.event_id, event.type, listing.id, JSON.stringify(event)]
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

Worker-ul de outbox poate publica în Kafka/RabbitMQ/SNS. Pentru Kafka, vezi documentația oficială: https://kafka.apache.org/documentation/

Flux concret: de la listare publicată la „transparență” pentru utilizator

Un flux realist într-o event-driven architecture:

  1. Listings emite ListingPublished
  2. Search consumă și indexează (Elastic/OpenSearch)
  3. Pricing/Insights calculează un „fair price band” (min/median/max) și emite ListingPriced
  4. Notifications trimite alertă pentru utilizatori care urmăresc „Golf 2017 diesel sub 13k”
  5. Audit/Compliance persistă un log imuabil (util pentru dispute)

Avantaj: dacă notificările sunt în mentenanță, listarea tot se publică. Când revii, refaci consumul din offset.

Observability: tracing și metrici pentru evenimente

În producție, întrebarea nu e „a mers?”, ci „unde s-a blocat?”.

Checklist pentru event-driven architecture:

  • trace_id propagat din request → eveniment → consumer
  • metrici pe consumer: lag, retry rate, DLQ rate
  • loguri structurale cu event_id și aggregate_id

OpenTelemetry e un standard solid pentru tracing distribuit: https://opentelemetry.io/

Pattern-uri utile: DLQ, retries, versioning

Retries cu backoff + Dead Letter Queue

  • retry pentru erori tranzitorii (timeout, rate-limit)
  • DLQ pentru payload invalid sau bug logic

Pseudo-config (conceptual):

consumer:
  retry:
    maxAttempts: 8
    backoff: exponential
  dlq:
    topic: events.dlq
Enter fullscreen mode Exit fullscreen mode

Versioning fără să rupi consumatorii

Reguli simple:

  • adaugi câmpuri noi ca opționale
  • nu schimbi semantica unui câmp existent
  • când e nevoie, crești schema_version și păstrezi compatibilitate o perioadă

What I learned / Gotchas (din implementări reale)

  • „Exact once” e rar realist end-to-end; tratează sistemul ca at-least-once și fă consumatori idempotent.
  • Outbox-ul devine un hotspot dacă nu cureți: pune retention (ex. șterge publicate > 7 zile) și monitorizează.
  • Evenimentele prea „grase” (payload enorm cu zeci de câmpuri) cresc costurile și fragilitatea; preferă payload-uri orientate pe use-case.
  • Dacă ai un proces de „dealer verificat”, evită să pui PII în evenimente; trimite referințe și fă lookup securizat.

Cum aș aplica asta pe AUTO101 (pași concreți)

Dacă construiești auto101.ro sau ceva similar, o adoptare incrementală a event-driven architecture ar arăta așa:

  1. Introdu outbox în serviciul de listări (cel mai mare ROI)
  2. Creează 2 consumatori separați: Search indexer și Notifications
  3. Adaugă idempotency + DLQ înainte să crești numărul de evenimente
  4. Instrumentează tracing cu OpenTelemetry și dashboard pentru lag
  5. Abia apoi extrage domenii (Leasing, Dealers) în servicii separate dacă ai presiune de scaling/ownership

Un CTA util: dacă ai deja un monolit și vrei să introduci evenimente fără rescriere totală, începe cu un singur flux (ex. ListingPublished) și măsoară impactul (timp de publicare, erori, lag). Apoi extinde contractul.


Originally about event-driven architecture.

Top comments (0)