A system design interview guide to building a pipeline that collects logs and events from thousands of services and lands them, without loss or duplication, in the stores that engineers and analysts actually query.
Student edition · start with the guarantee
Move structured events from applications to independent searchable, analytical, and archival sinks with a bounded loss/replay policy.
Correctness contract: Accepted records have a documented durability boundary. Each sink advances its own checkpoint only after its required durable writes; overload and quarantine are visible.
Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.
Almost every company eventually arrives at the same realization: the data describing how the system behaves — request logs, error traces, business events — is at least as valuable as the data the system was built to serve. A logging or analytics pipeline is the plumbing that captures that exhaust from every running process and delivers it to the places people query it: a search store for debugging an incident at 3 a.m., a warehouse for the analyst building a dashboard, and cheap archival storage for the auditor who needs last year's records. The naive approach — have each service write straight to the destination — collapses the moment you have many producers, bursty traffic, or more than one consumer. This guide builds the pipeline up the way it actually has to be built: collect at the edge, buffer in the middle, process the stream, and fan out to several sinks, with delivery guarantees and ordering handled deliberately at each step.
Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.
Pub/sub and durable queues
Publish/subscribe routes an event to interested subscribers; it may be ephemeral or durable. A queue/log with retained records and offsets allows replay after a consumer is disconnected.
Example. An online gateway misses a comment while restarting. Ephemeral Redis Pub/Sub cannot replay it; a durable comment log can fetch after the gateway's last sequence.
Limit. Pub/sub alone does not promise persistence, global order, or exactly-once effects. Broker choice, acknowledgements, retry horizon, and client catch-up protocol define those guarantees.
Offsets and checkpoints
An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed.
Example. If a batch increments a total and crashes before saving its offset, replay increments twice. Commit the total, dedup state, and partition position together, or use a compatible transactional/idempotent sink.
Limit. Saving an offset after a write avoids that specific loss window but can still duplicate; framework checkpoints alone do not make arbitrary external effects transactional.
Write-ahead log and fsync
A WAL records updates before data pages are considered committed. Recovery replays it after a crash. fsync (or an equivalent storage contract) requests that buffered bytes reach durable storage rather than only process/OS memory.
Example. A server acknowledges after its WAL commit and required replica writes are durably accepted. A process crash can then reconstruct the committed update.
Limit. Appending to a file alone does not guarantee power-loss durability. Device behavior, filesystem ordering, replicas, corruption, and the declared fault budget still matter.
Backpressure and load shedding
Backpressure tells an upstream producer that the downstream stage cannot accept more work. Buffering delays that signal; shedding deliberately rejects or drops lower-priority work when a bound is reached.
Example. A sink accepts 100 events/s while input is 150/s. A 1000-event queue fills in 20 seconds. The system must then block, reject, or apply its documented drop policy.
Limit. A finite queue cannot absorb an infinite outage. Prioritized messages still need hard quotas, deadlines, and bounded client queues; otherwise an exemption can exhaust the system.
Deduplication
Recognizing multiple deliveries of one logical event using a stable identity. The same logical event must keep its ID across retries, and claiming the ID must be atomic with the counted or stored effect.
Example. Events e1,e2,e1 should yield a count of 2. An exact seen set plus the increment in one transaction makes the second e1 a no-op.
Limit. A TTL limits the dedup horizon; retries after expiry may count again. Approximate filters may drop new events, and changing the ID or routing a retry to another local seen set defeats dedup.
Transactional outbox
A business transaction writes its state change and an event row to the same database. A relay publishes committed event rows later, retrying until the broker durably accepts them.
Example. An order and its OrderCreated outbox row commit together. A relay crash after publish but before marking sent publishes the same event ID again on restart.
Limit. The relay is at-least-once, not magically exactly-once. Consumers dedup transactionally; preserve per-entity sequence when order matters. CDC is change data capture: reading committed database changes.
1. Requirements and Clarifying Questions
How many hosts and how much data? (Assume 20,000 hosts, about 100 lines per second each.)
Who reads it? Engineers searching recent logs, analysts querying events, auditors needing old data. (Assume all three.)
How fresh? (Assume logs searchable within 30 seconds; analytics within minutes.)
Can we lose data? Debug logs: a little under extreme load. Billing and audit events: never.
How long to keep it? (Assume 7 days hot search, 1 year warehouse, 7 years archive for compliance.)
Functional
Non-functional
Collect logs and events from every host without app changes.
Sustain 2 M lines/s (600 MB/s) with headroom for spikes.
Full-text search over recent logs.
Search freshness under 30 s; queries in seconds.
Deliver structured events to the warehouse.
No loss of events; duplicates removed.
Archive everything cheaply; replay on demand.
Applications never block on the pipeline.
2. Back-of-the-Envelope Estimation
Assume 20,000 hosts × 100 lines/s, 300 bytes per line, 5× compression, Kafka replication factor 3 with 3 days of retention, and a search store that indexes 20% of the data (warnings and above, plus sampled info).
Step
Working
Result
Lines per second
20,000 hosts × 100
2 M /s
Raw bytes per second
2 M × 300 B
600 MB/s
Raw bytes per day
600 MB/s × 86,400 s
≈ 51.8 TB /day
Compressed per day
51.8 TB ÷ 5
≈ 10.4 TB /day
Kafka disk
10.4 TB × 3 days × RF 3
≈ 93 TB
Kafka partitions
600 MB/s ÷ ~10 MB/s per consumer
60 → use 128
Search store
51.8 TB × 20% × 7 days × 2 copies
≈ 145 TB
6-hour outage backlog
600 MB/s × 21,600 s (raw)
≈ 13 TB
Archive growth
10.4 TB × 365
≈ 3.8 PB /year
Two things stand out. The search store is the expensive sink, which is why routing and sampling matter. And a 6-hour sink outage builds a 13 TB backlog; with consumers able to run at 2× the input rate, the extra capacity (600 MB/s) drains it in about 6 more hours. That fits easily inside 3 days of retention.
3. Sources of Data
The first thing to be precise about is what is actually flowing into the pipeline, because the two main kinds of data have different shapes and different downstream uses. Lumping them together as "logs" hides design decisions you will later wish you had made on purpose.
Kind
Example
Shape
Primarily for
Application logs
"connection refused, retrying", a stack trace
Often free-form text lines, sometimes with a severity and timestamp
Debugging and incident response
Structured events
"checkout_completed" with amount, user, currency
A typed record with named fields, ideally schema-validated
Analytics, metrics, billing
Application logs are what a human reads when something breaks; they are forgiving in format but enormous in volume. Structured events are what a query engine aggregates; they need consistent field names and types or every downstream analysis becomes a guessing game. A good pipeline accepts both but nudges producers toward structure: even a log line is far more useful when it carries a service name, a timestamp, a severity, and a trace id as proper fields rather than buried in a string. The earlier you impose a little structure, the less parsing and guesswork everything downstream has to do.
A useful interview framing: you are not building "a place to put logs," you are building a transport for two related-but-different data streams — high-volume diagnostic text and typed analytic events — that share one ingestion path but diverge at the sinks. Keeping that distinction in mind explains many later choices.
4. Record Format and Interfaces
A pipeline's "API" is mostly its record envelope. Agree on it early; every stage depends on it.
{
"event_id": "01J8Z3K4Q6…", # unique, set once at the source (dedup key)
"ts": "2026-09-25T14:03:07.412Z", # event time
"service": "checkout", # also the partition key
"host": "web-4411", "region": "us-east-1",
"severity": "ERROR",
"trace_id": "4bf92f35…", # links logs to traces
"schema": "checkout_completed/v3",# structured events only
"body": { … } # text line or typed fields
}
Interface
Shape
Notes
App → agent
Write to a local file or local socket
Never a network call from the app.
Agent → broker
Batched, compressed produce to topic logs.<service> or events.<type>
acks=all for events; acks=1 is acceptable for debug logs.
Search query
GET /search?q=service:checkout AND severity:ERROR&from=-15m
Hot data only.
Warehouse
Partitioned tables, e.g. events.checkout_completed by dt, hour
Columnar files (Parquet) in the lake.
5. The Collection Agent
Once you accept that data is generated on thousands of hosts, the question becomes how to get it off those hosts reliably without coupling every application to the pipeline. The standard answer is a collection agent: a small, separate process that runs on each host (or as a sidecar next to each container), tails the application's log files or receives events over a local socket, and forwards them onward.
Running collection as a separate agent rather than inside the application buys a clean separation of concerns. The application's only job is to emit a line or an event locally and move on; it never blocks on the network and never needs to know where the data eventually goes. The agent handles the messy realities of shipping data: batching many small records into efficient payloads, compressing them, retrying when the next hop is unavailable, and buffering to local disk so that a brief outage downstream does not lose data or stall the application. Because the agent is uniform across the fleet, you can also use it to attach common metadata — host name, region, environment, service version — to every record without touching application code.
# agent runs on every host, tails sources and forwards in batches
function agent_loop():
buffer = []
for record in tail(log_files) + recv(local_socket):
record = enrich(record, host=hostname(), region=REGION)
buffer.append(record)
if buffer.full() or timer.expired():
try:
broker.send(compress(buffer)) # one batched, compressed call
buffer.clear()
except Unavailable:
spill_to_disk(buffer) # survive a downstream outage
buffer.clear() # spilled batches are resent by replay_spill()
function replay_spill(): # background thread
for batch in oldest_first(spill_dir):
broker.send(batch) # retry with backoff until it succeeds
delete(batch) # only after the broker acknowledged it
Local disk is finite too
The spill directory has a size cap. If the broker is down longer than the disk can cover, the agent must choose: drop the oldest, drop low-severity lines, or sample. Decide this on purpose and emit a metric for every dropped record, so loss is visible instead of silent. Also resend spilled batches with their original event ids, so a batch that was actually delivered before the error is deduplicated downstream.
6. End-to-End Architecture
With sources and agents established, the central design is the path a record takes from the host where it is born to the store where it is finally queried. The defining move is to insert a durable buffer between the producers and the consumers, so that the side generating data and the side reading it are completely decoupled and can fail, scale, or slow down independently.
Sources (application logs and structured events) are picked up by a collection agent running on each host. The agent forwards batches to a durable, partitioned broker such as Kafka, which decouples producers from consumers and absorbs spikes. Stream processors consume from the broker to parse, enrich, and route records, then write them to the sinks — a search store, a data warehouse, and cold archival storage.
Reading the flow left to right:
Sources and agents. Every host produces logs and events; its local agent batches, compresses, and ships them, so applications are never coupled to the rest of the pipeline.
Durable broker. A partitioned, replicated log such as Kafka sits in the middle. Producers append to it and consumers read from it independently. It is the shock absorber: it persists records for a retention window, absorbs traffic spikes, and lets slow or restarting consumers catch up without dropping anything.
Stream processors. A pool of consumers reads from the broker and does the real work — parsing raw lines into fields, enriching records with extra context, filtering noise, and routing each record to the sinks that should receive it.
Sinks. The processed stream lands in several destinations at once, each tuned to a different access pattern: a search store for interactive debugging, a warehouse or lake for analytics, and cold object storage for long-term archive.
The single most important property of this shape is that the broker breaks the system into two halves that can be reasoned about — and operated — separately. A spike in producers, a slow sink, or a redeploy of the processors no longer ripples across the whole pipeline.
7. The Durable Buffer
The broker deserves its own section because it is what turns a fragile chain into a resilient system. Without it, every producer is directly coupled to every consumer: if the warehouse is slow, the applications writing to it slow down too, and a traffic spike that the sinks cannot absorb is simply lost. Putting a durable, append-only log in the middle changes all of that.
Decoupling. Producers only need to know about the broker, and consumers only need to know about the broker. Either side can be redeployed, scaled, or fail without the other noticing. You can even add a brand-new consumer later and have it read the existing data — but only data still inside the broker's retention window (often 3–7 days). Anything older must be backfilled from the archive.
Spike absorption. When events arrive faster than the sinks can accept them, the excess simply accumulates in the broker for its retention period and drains once the burst passes. The pipeline degrades into latency, not loss — as long as the backlog is drained before retention deletes it.
Durability and replay. Because the broker persists and replicates records, a consumer that crashes can resume from its last committed position, and you can replay history — for example to backfill a new sink or to recover from a bug in a processor — by rewinding the read position.
Kafka deletes old segments on a timer, whether or not anyone has read them. While the consumer's lag stays inside the retention window, a slow sink only adds latency. Once lag passes retention, the oldest unread records are deleted: that is silent data loss.
The broker is what converts "we have many producers and several fragile sinks" into "everyone talks to one durable log." It is the structural reason a slow warehouse becomes a growing backlog you can watch and drain, rather than data silently dropped at the edge.
8. Stream Processing
Raw records as they leave the agent are rarely in the form the sinks want. The stream processing layer is where they are transformed on the fly, record by record, as they flow from the broker to the sinks. Three jobs dominate this stage.
Parse. Turn a raw log line into structured fields — extract the timestamp, severity, message, and any embedded key-value pairs — so that downstream stores can index and query them rather than treating each entry as opaque text.
Enrich. Add context the source could not include: resolve a user id to a region, attach a deployment version, tag records with the data center, or join against a lookup table. Enrichment is what makes later analysis possible without re-deriving the same facts everywhere.
Route and filter. Decide where each record goes. Debug-level chatter might go only to the search store with a short retention; billing events must go to the warehouse and the archive. Pure noise can be dropped here so it never costs storage downstream.
function process(record):
record = parse(record) # text -> typed fields
record = enrich(record, lookup_region) # add context
if is_noise(record):
return # drop, don't store
for sink in route(record): # fan out to chosen sinks
sink.write(record)
Because processing runs as a horizontally scalable pool of consumers reading from the broker's partitions, it scales with traffic simply by adding more instances — up to the number of partitions, which is why partition count, discussed below, is a capacity decision and not just a correctness one.
9. Sinks and Fan-Out
The same processed record usually needs to live in more than one place, because no single store is good at everything. This is the fan-out: one logical stream is written to several sinks, each chosen for a different access pattern, cost profile, and retention.
A single processed stream is fanned out to three sinks. A search / observability store serves interactive debugging, a data warehouse or lake serves analytics and BI, and cold object storage holds a cheap long-term archive. Each receives the same data but keeps it differently — different retention, cost, and query pattern.
Treating these as separate sinks rather than trying to force one store to do all three is what keeps each one affordable and fast at its job. The search store stays small and fast because it only holds recent hot data; the warehouse holds the structured events analysts care about; the archive holds everything cheaply for as long as policy requires. Routing decisions made in the processing layer determine which records reach which sink, so you are not paying search-store prices to store a year of debug logs.
10. Delivery Guarantees
At-least-once processing advances a sink's input position only after the required write is durably accepted. It permits redelivery after uncertain acknowledgements or crashes. It does not promise survival of arbitrary outages, retention expiry, source loss, or loss of all durable copies. At-most-once may be a deliberate policy for low-value debug telemetry, not for accepted audit events.
Kafka idempotent producers deduplicate supported producer retries, not arbitrary re-emission as a fresh logical event. Kafka transactions can atomically publish output records and input offsets inside Kafka; consumers must use the appropriate committed isolation. An external database/object/search sink needs its own transactional or idempotent publication contract.
Use stable source event IDs, explicit per-sink consumer groups/checkpoints, and a raw archive for longer replay. Approximate/windowed dedup is acceptable only when the consumer permits its undercount/duplicate horizon. The collection agent must persist source position and spill state in crash-safe order; a partially sent unacknowledged batch needs idempotent replay, not silent deletion.
11. Partitioning and Backpressure
Two operational mechanisms make the pipeline both fast and stable: partitioning, which buys ordering and parallelism, and backpressure, which keeps a fast producer from drowning a slow consumer.
Partitioning. The broker splits each topic into partitions. Records are assigned to a partition by hashing a key — for example the service name or the user id. This gives two properties at once: records that share a key always land in the same partition and are therefore consumed in order, while different keys spread across partitions and are consumed in parallel. The number of partitions is the ceiling on how many consumers can work a topic simultaneously, so it is the main lever for throughput.
A producer assigns each record to a partition by hashing a key (here, the service id). Records with the same key always land in the same partition, preserving their order, while different keys spread across partitions so that one consumer per partition can process them in parallel. More partitions means more parallel consumers.
Backpressure. Because the broker decouples the two sides, a slow sink does not stall producers; instead the consumer's lag — how far its committed position trails the newest record — grows. That lag is the system's primary health signal. Rising lag tells you a sink or the processor pool is the bottleneck, and it gives you a graceful place to react: add consumers, shed or sample low-value records, or let the broker's retention hold the backlog until the burst passes. The pipeline turns overload into measurable latency rather than silent data loss — until lag grows past retention, at which point Kafka deletes records nobody has read yet (see the diagram above). So alert on lag measured in time ("the consumer is 9 hours behind") and compare it with retention.
Strictly speaking, this is buffering. True backpressure means the slow side pushes back on the fast side: if the brokers themselves are overloaded, producer sends block or time out, the agents' buffers fill, and the agents must spill, sample, or drop. A good design says which of those happens, where, and how it is measured.
Partition count is a decision you make early and regret changing late: it caps parallelism, and re-partitioning a live topic disturbs key-to-partition ordering. Size it for peak throughput with headroom, and pick a partition key that spreads load evenly without splitting records that must stay ordered together.
12. Schema Management
A schema is the producer/consumer contract for field names, types, defaults, and meaning. Backward compatibility means a new reader can consume old data; forward compatibility means an old reader can consume new data. Full compatibility requires both under the selected serialization rules and version scope.
An optional field is safe only if defaults, unknown-field handling, and reader expectations allow it. Avro, Protobuf, and JSON Schema rules differ; a registry is not automatically a semantic validator. Version meanings, units, and enum behavior as well as shapes. Edge checks quarantine invalid records with access control, bounded storage, and a replay owner rather than poisoning aggregates.
13. Failure Modes
Failure
What happens
Mitigation
Broker cluster unreachable
Agents cannot send.
Agents spill to local disk, replay later; drop policy and metrics once disk fills.
Broker node dies
Its partitions fail over.
RF 3, min.insync.replicas=2, acks=all for events that must not be lost.
Search store slow or down
Its consumer lag grows.
Other sinks keep flowing (separate consumer groups). Alert on lag in hours versus retention.
Lag exceeds retention
Unread data deleted.
Scale consumers, raise retention temporarily, or backfill from the archive.
Poison record crashes the parser
Consumer loops on it forever.
Catch errors per record; send failures to a dead-letter topic with the error.
Producer ships a breaking schema
Warehouse columns go wrong.
Registry compatibility check rejects it at produce time.
Log storm (a bug logs in a tight loop)
One service floods the pipeline.
Per-service rate limits and sampling in the agent; quotas on the broker.
14. Tradeoffs and Alternatives
Choice
Option A
Option B
Pick when
Transport
Agents write straight to sinks
Agents → Kafka → sinks
A only for small fleets. B once there are several sinks or spikes.
Delivery
At-most-once (fire and forget)
At-least-once + idempotent sinks
A for noisy debug logs under load. B for events and audit.
Parsing
At the agent
In stream processors
Agent keeps the broker structured; processors are easier to update centrally.
Search index
Full-text index everything (Elasticsearch/OpenSearch)
Index labels only, grep chunks (Loki style)
A for rich search; B for far cheaper storage when queries filter by service/time first.
Retention
Long in Kafka
Short in Kafka, replay from archive
Longer Kafka retention makes recovery simple but costs disk.
Worked example: draining a six-hour outage
Assume raw ingress 600 MB/s, a sink outage of six hours, and recovered service capacity of 1200 MB/s.
Read top to bottom. The table below explains the state and guarantee at each step.
Step
State, calculation, and reasoning
1. Build a backlog
600×21,600=12,960,000 MB≈12.96 TB raw accumulates. Compression/replication change stored bytes, not logical backlog.
2. Restart at the last committed position
A sink retry may repeat a written record; an event-ID upsert or transactional contribution makes that repetition safe.
3. Continue serving new ingress
Net drain is 1200−600=600 MB/s, so catching up takes another six hours under the constant-rate assumption.
4. Protect the retention boundary
A three-day age policy is not an unconditional guarantee. Check oldest unread age, size retention, disk limits, and enough recovery headroom before old records expire.
Check your understanding: Does writing before committing the offset make loss impossible?
It closes the crash-before-write/offset gap only under retained durable input and durable sink assumptions. It can duplicate, and retention, producer loss, or sink data loss can still break the end-to-end promise.
Run a small, testable model
Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:
python3 concepts.py dedup
python3 concepts.py test
Use the transactional contribution example to see why replay can be safe even when delivery repeats.
The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.
Committing the offset before the write. A crash then loses the record.
Letting the app call the network to log. A slow pipeline then slows the app.
One consumer group for all sinks. A slow sink then stalls fast ones. Give each sink its own group.
Indexing everything in the search store. It is the most expensive sink. Route and sample.
17. Summary
A logging and analytics pipeline is a decoupled transport that collects data at the edge, buffers it durably, transforms it in flight, and fans it out to purpose-built stores. Its design is a handful of reinforcing decisions:
Concern
Mechanism
What flows in?
Two streams — high-volume application logs and typed structured events — sharing one ingestion path.
How does data leave the hosts?
A collection agent per host: batches, compresses, enriches, retries, and buffers locally.
How are producers decoupled from consumers?
A durable, partitioned broker (e.g. Kafka) in the middle that persists, replicates, and absorbs spikes.
How is raw data made useful?
Stream processors parse, enrich, filter, and route each record as it flows through.
Where does it land?
Fan-out to several sinks: a search store, a warehouse/lake, and cold object storage, each tuned differently.
How do we avoid loss and duplicates?
At-least-once delivery (commit after write) plus dedup on a stable per-record id.
How do we get ordering and parallelism?
Partition by key: same key in order, different keys in parallel; partition count caps throughput.
How do we handle overload?
Buffer in the broker and watch consumer lag — overload becomes measurable latency, not loss, as long as lag stays under retention.
How do we keep data correct over time?
A schema registry with compatible evolution and validation at the edge.
The recurring theme: a durable buffer in the middle lets the producing and consuming halves of the system fail, scale, and evolve independently. Everything else — agents, partitioning, at-least-once plus dedup, schemas, fan-out — exists to make that decoupled flow correct, ordered, and affordable.
18. Check Yourself
A consumer writes a batch to the warehouse and crashes before committing its offset. What happens after restart, and why is that acceptable?
Answer
It re-reads and re-writes the batch. Because writes are idempotent upserts keyed by event_id, the replayed records overwrite themselves and nothing is double counted.
Kafka retention is 3 days. The search sink has been down for 4 days. What happened to the data?
Answer
The oldest day of unread records was deleted by retention before the search consumer read it. Recover it from the archive, and alert on lag-in-time well before it reaches retention.
Your topic has 24 partitions. You add 40 consumers in one group. How many do useful work?
Answer
24. Each partition is read by at most one consumer in a group, so 16 sit idle. Partition count caps parallelism.
Why do the search store and the warehouse use separate consumer groups?
Answer
Each group tracks its own offsets. A slow search store builds its own lag without holding back the warehouse.
A producer renames amount to total. What stops this from breaking dashboards?
Answer
The schema registry's compatibility check rejects the new schema at produce time. The safe path is to add total as a new optional field, migrate readers, then retire amount.
At 600 MB/s, how much backlog does a 6-hour sink outage create, and how long to drain it at 2× capacity?
Answer
600 MB/s × 21,600 s ≈ 13 TB. The spare capacity is 600 MB/s, so it takes about 6 more hours to catch up.
19. Key Terms
Collection agent
Per-host process that tails logs and ships batches (Fluent Bit, Vector, Filebeat).
Broker
Durable, partitioned, replicated log between producers and consumers (Kafka).
Partition
An ordered shard of a topic; the unit of parallelism.
Offset
An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed. Example and limits above.
Consumer group
Consumers sharing one set of offsets; each partition goes to one member.
Consumer lag
How far a consumer trails the newest record, in records or time.
Retention
How long the broker keeps records before deleting them, read or not.
At-least-once
No loss, possible duplicates.
Effectively-once
At-least-once delivery plus idempotent writes, so duplicates have no effect.
Dead-letter topic
Where records that fail processing are parked for inspection.
Schema registry
Central store of event schemas and compatibility rules.
Further reading
Jay Kreps, "The Log: What every software engineer should know about real-time data's unifying abstraction" (2013).
Apache Kafka documentation: design, message delivery semantics, and transactions.
Martin Kleppmann, Designing Data-Intensive Applications, chapter 4 (encoding and evolution) and chapter 11 (stream processing).
Confluent Schema Registry documentation: schema evolution and compatibility types.