Skip to content
Work/Case Study
Event-Driven Architecture · Reliability

The Kafka Event Platform

We were losing audit data one HTTP call at a time — silently, and worst exactly when we were busiest. How I moved the platform onto a durable event backbone without a single flag-day cutover.

Role
Design + built the consumer from scratch + cross-service rollout
Stack
Kafka · NestJS · Couchbase · OpenTelemetry
Timeframe
2025 → 2026
Headline result
silent data loss → 0 · 14.6M+ msgs
OwnershipImplementation · HighDesign · HighTech Leadership · HighOperational · HighConfidence · Very High
01 — Background

Every change to an appointment is worth remembering — a reschedule, a status change, a room move, a cancellation. Each is a log entry, and those entries are how the product answers "what happened to this booking, and when?"

The volume is the story. North of 3,500 active clinics; for each, an automated hook reconciles appointments every few minutes; a single request can carry a couple hundred appointments, each generating a log entry. Multiply it out and you get a firehose of small writes that never stops.

Silent data loss is the failure
that never pages you.

02 — Problem

Each log entry was written with its own synchronous HTTP call to a dedicated logging service. Fine in a code review; fine at low volume. But one reconciliation request carrying 200 appointments fanned out into 200 blocking calls before it could finish — times thousands of clinics, on a few-minute cadence.

Under that load the logging service fell over. And a synchronous call to a service that isn't answering doesn't queue anything — it fails, and the data evaporates. No buffer, no retry, no durable place to wait. The loss clustered exactly when volume was highest.

Fig 1 · The failure — a non-critical write on the critical path
Reconcile request
~200 appointments
×200
Sync HTTP calls
blocking, one per log
Logging service
down under load
no buffer · no retry · no durable wait→ logs silently lost
The deeper flaw wasn't sync-vs-async. It was a durable-but-latency-tolerant write chained to a latency-sensitive business path.
03 — Investigation

This wasn't a flame-graph hunt. The failure mode was understood; the work was confirming shape and scale before proposing a rewrite. I traced the call path and confirmed two things.

One: the write really was per-record and synchronous, on the request path. Two — a gift — the exact same log payload was already used in several places across the platform.

That second fact shaped the entire migration. The measurement that mattered wasn't a profiler — it was understanding the failure honestly: what exactly is lost, when, and why.

04 — Alternatives considered

Which options you reject says more than the one you pick.

Scale the logging service

Rejected

Treats the symptom. The coupling remains — a degraded store still back-pressures the business path. You can't scale out of "the fast path is chained to the slow path."

Fire-and-forget async in-process

Rejected

Fixes latency, worsens loss — failures become fully invisible, still no durable buffer. Speed without durability is the wrong trade for audit data.

Persistent outbox, exactly-once

Rejected (for now)

The most robust on paper — and I took it seriously. But it couples logging back into the business store's transactions, and the guarantee it buys was stronger than this data needs. I noted exactly where I'd reach for it if requirements changed.

Durable event log + dedicated async consumer

Chosen

Publish each event to a durable, replayable log; a separate service consumes and persists asynchronously. The log absorbs bursts, survives a down consumer, and scales independently.

05 — Decision

Produce to a durable log, consume asynchronously, persist idempotently. Three moves, each decoupling the fast work from the durable work.

1

Producer: publish, don't call

Services publish the log event to a durable stream instead of a cross-service HTTP round-trip — wrapped in a bounded in-memory buffer, exponential backoff, auto-reconnect, and a graceful drain on shutdown.

2

Consumer: a service built from scratch

A dedicated NestJS service whose only job is to consume events and persist them — isolated failures, consumer-group parallelism so throughput grows by adding consumers.

3

Sink: idempotent, unique-keyed writes

Every event carries a deterministic key; the consumer upserts on it. Redelivery upserts the same document — no duplicates, by construction.

06 — Architecture

The durable log sits between the fast work and the slow work.

Producers publish and move on. The topic absorbs bursts and survives a down consumer. The consumer group persists on its own schedule, idempotently.

Fig 2 · The eventing backbone
appointment service
sync service
…more services
producers · bounded queue + backoff
publish
log-events topic
durable Kafka topic
many partitionsreplicated
consume
Consumer group
built from scratch · scales by replica
↓ upsert
Couchbase
idempotent · unique key
If persistence is slow or down, events wait in the topic instead of vanishing. That single property is the whole point.
Delivery model · scrutinize this

I did not build exactly-once. I built at-least-once made safe by idempotent writes — the pragmatic sweet spot, without the cost of guaranteeing uniqueness end-to-end.

Fig 3 · Why redelivery is harmless
event · key=appt_991·a3f
event · key=appt_991·a3f (redelivered)
upsert
1 document
same key → no duplicate

append-only, immutable records with unique identities → at-least-once is safe by construction

You get the robustness people reach for exactly-once to achieve, at a fraction of the cost. Reserve exactly-once for the rare cases that truly can't tolerate a duplicate.

Exactly-once wasn't the answer.
Idempotency was.

Implementation · the migration

The detail I'm proudest of is what made it safe to ship: the migration was drop-in because I kept the payload identical. The new event carried the exact same shape as the old HTTP body — so switching a service from "call" to "publish" was a small, reversible change with no downstream schema impact.

one core service · both paths coexisted during rollout
// old — synchronous, dropped data under load
logChange(entry)   → HTTP POST   logging service

// new — durable, drop-in (same payload)
logChange(entry)   → publish      log-events topic

Services were moved over gradually, verified, and only then was the old path retired. No big-bang, no flag day — you make the two worlds coexist and walk the traffic across.

07 — Trade-offs
Chose

Eventual consistency

Not buying
Synchronous immediacy — logs land ms–s behind the mutation. Easy trade for audit data; wrong for something read back instantly.
Chose

Bounded in-memory buffer

Not buying
An outbox. A hard process kill could lose what's buffered but unpublished — I documented the exact condition under which I'd upgrade.
Chose

A second service + topic

Not buying
Simplicity of one process — async moves complexity from the code path to the operational surface. Real, and worth it here.

There's a failed-events topic too — honestly, a safety net created during testing that production hardening made unnecessary, not an actively-drained DLQ. I'd rather say that than dress it up.

08 — Outcome
production dashboardin progress
0

Silent data-loss events. The business path no longer depends on the logging store's health; the topic absorbs bursts.

eliminated
0.0M+

Messages retained across a well-partitioned, replicated topic — an eight-figure-message backbone where before we had a call that dropped data.

prod dashboard
3,500+

Active clinics whose every appointment change now lands durably, instead of dropping under load.

prod dashboard

The honest part: under peak load the consumer doesn't fully keep pace — meaningful lag, traced to one-at-a-time upserts and running fewer consumers than partitions. We scaled consumers up once as a first mitigation; the real remediation — batched writes + scaling toward the partition count — is in progress. A known, diagnosed, addressable bottleneck, not a mystery.

09 — Lessons learned
Separate "must be durable" from "must be fast." The root cause wasn't Kafka-vs-HTTP — it was a durable, latency-tolerant write chained to a latency-sensitive path. See writes through that lens and the fix designs itself.
Silent data loss is the failure to fear most. Slow pages page you; errors page you; lost audit data doesn't. "What happens to this data if the destination is down?" is now a first-class question for me.
At-least-once + idempotency beats chasing exactly-once. Deterministic keys and an idempotent sink give the robustness people reach for exactly-once to get, at a fraction of the cost.
Backward-compatible payloads make migrations survivable. Not redesigning the message turned a scary platform-wide migration into a series of small, reversible, drop-in changes.
10 — If I rebuilt this today

The core design I'd keep. Three things operating it taught me to do differently.

Change

Batch writes from day one

The single biggest cause of consumer lag is one record per round-trip. Accumulate N, write in one bulk op — obvious in hindsight, and what's being built now.

Change

Match consumers to partitions by default

Under-utilizing partitions leaves free throughput on the table. Make parallelism a deployment default so the pipeline scales without someone bumping a number.

Learned

Lag + throughput dashboards before shipping

The lag was found by looking, not by an alert — that ordering is backwards. Observability belongs in the definition of done, so the pipeline tells you when it's falling behind.

© 2026 Sukhcharan SinghCase study · The Kafka Event Platform