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) șioccurred_at - include
aggregate_id+aggregate_type - include
schema_version - include
trace_idpentru 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"
}
}
}
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_idexistă, 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;
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)]
);
});
}
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:
-
Listings emite
ListingPublished - Search consumă și indexează (Elastic/OpenSearch)
-
Pricing/Insights calculează un „fair price band” (min/median/max) și emite
ListingPriced - Notifications trimite alertă pentru utilizatori care urmăresc „Golf 2017 diesel sub 13k”
- 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șiaggregate_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
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:
- Introdu outbox în serviciul de listări (cel mai mare ROI)
- Creează 2 consumatori separați: Search indexer și Notifications
- Adaugă idempotency + DLQ înainte să crești numărul de evenimente
- Instrumentează tracing cu OpenTelemetry și dashboard pentru lag
- 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)