0% completed
Designing an Event
On This Page
Naming an Event
Identifiers and Time
Choosing the Key
Thin Events and Full Events
Headers
The Event, Redesigned
Using This in an Interview
Key Takeaways
Practice Questions
Here is an event that a hotel booking service publishes when a guest confirms a reservation:
{"event":"update","id":"B-5512","status":"CONFIRMED","changed":"10/03 14:22"}
It looks reasonable, and it causes problems for every service that reads it. The name update does not say what happened, so each reader has to compare status with what it saw before. There is no identifier for the event itself, so a reader cannot tell a repeated delivery from a second update. The time 10/03 means 10 March in one country and 3 October in another, with no time zone. A reader that needs the room type or the price must call the booking service to get it.
Each of those problems comes from a design decision that was never made. This page makes them one at a time: the name, the identifiers and time, the key, how much data to carry, and what belongs in headers.
Naming an Event
An event's name should state one business fact, in the past tense. RoomBooked, BookingCancelled, and GuestCheckedIn each tell a reader exactly what happened without opening the payload.
A generic name such as BookingUpdated pushes work onto every reader. The loyalty service cares only about stays, and the housekeeping service cares only about check-outs, yet both must read every update and work out which fields changed. With one event type per fact, each reader ignores the types it does not need.
The past tense also separates events from commands. BookRoom asks a service to do something and can be refused. RoomBooked reports something that is already true, so readers react to it rather than approve it.
Identifiers and Time
Every event needs three values that its readers can rely on:
- An event ID, unique to this event. Kafka delivers at least once by default, so a reader can receive the same event twice. If both copies carry the same event ID, the reader can recognize the second one and skip it.
- The ID of the thing it describes, here the booking ID
B-5512. Readers use it to find their own record of that booking. - The time it happened, in an unambiguous form. Use a count of milliseconds since the Unix epoch, which is defined in UTC, or a full date and time with an explicit zone. Kafka attaches its own timestamp to every record, but the time the guest confirmed the booking is a business fact and belongs in the event.
Choosing the Key
Every Kafka record can carry a key, and Kafka's documentation states that "events with the same key ... are written to the same partition." Each partition is "totally ordered," so all the events for one key are read in the order they were written.
Key an event by the thing whose events must stay in order. For bookings, that is the booking ID. A BookingCancelled that is read before its own RoomBooked would leave a reader holding a booking that should not exist, and keying both by B-5512 prevents that.
Other keys change what is ordered. Keying by hotel ID keeps every booking at one hotel in order, but it sends all of a 900-room hotel's traffic to a single partition. Sending events with no key spreads them across partitions and gives up order for any single booking.
Thin Events and Full Events
A thin event carries only identifiers; a full event carries the facts that readers need. A thin RoomBooked says "booking B-5512 changed, ask the booking service for details." A full RoomBooked includes the hotel, the room type, the dates, and the price.
Thin events cost the booking service extra load. Here is a simple example. The hotel chain confirms 50 bookings a second at peak, and four services read the event. If every reader calls back for details, the booking service receives 200 extra requests a second that a full event would have avoided.
Thin events can also give a reader the wrong state. A reader that is 10 minutes behind calls back for B-5512 and receives today's state, which may already be cancelled. It then handles the RoomBooked event with data that describes a cancelled booking.
Full events cost space and carry risk. The table below compares the two RoomBooked events from the program later on this page, at 50 events a second, which is 4.32 million events a day. Storage assumes 7 days of retention and three copies of every event.
| Thin event | Full event | |
|---|---|---|
| Size of one event | 41 bytes | 212 bytes |
| Per day | About 177 MB | About 916 MB |
| 7 days, 3 copies | About 3.7 GB | About 19.2 GB |
| Calls back to the booking service | 200 a second | None |
| State a lagging reader sees | The current state, which may have changed | The state at the moment of booking |
Size limits are rarely the problem for events like these. The default maximum for a record batch is 1,048,588 bytes, about 1 MB, so a 212-byte event is far below it. Large content such as a scanned passport or a PDF should be stored elsewhere and referenced by a link.
The risk is personal data. A full event copies whatever it carries into every reader and keeps it in the topic for the whole retention time. Carrying the guest ID G-31877 instead of the guest's email address and phone number keeps that data in one service.
The usual answer is between the two extremes: carry the facts of this change, plus identifiers for everything else. RoomBooked should include the room type, dates, and price, because they are what was booked. It should not include the guest's full profile, which belongs to a different service and changes on its own schedule.
Headers
Besides the key and the value, a Kafka record can carry headers, which Kafka's documentation describes as "optional metadata headers." Headers are a good place for values that readers use to route or deduplicate an event before parsing it, such as the event type and the event ID.
The program below publishes a thin and a full RoomBooked for B-5512, a BookingCancelled for the same booking, and a RoomBooked for another booking, to a topic with three partitions. Each record carries two headers.
static void send(KafkaProducer<String, String> producer, String label, String key, String type, String eventId, String value) throws Exception { ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, key, value); record.headers().add("event-type", type.getBytes(StandardCharsets.UTF_8)); record.headers().add("event-id", eventId.getBytes(StandardCharsets.UTF_8)); RecordMetadata m = producer.send(record).get(); System.out.println(label + ": " + m.serializedValueSize() + " bytes, partition " + m.partition()); }
A reader then reads partition 1 from its first offset and prints each record's event-type header. Running it against a single Kafka 4.4 broker printed the following.
Output:
thin RoomBooked: 41 bytes, partition 1 full RoomBooked: 212 bytes, partition 1 BookingCancelled: 41 bytes, partition 1 RoomBooked for another booking: 41 bytes, partition 2 partition 1, offset 0: RoomBooked for B-5512 partition 1, offset 1: RoomBooked for B-5512 partition 1, offset 2: BookingCancelled for B-5512
All three events keyed B-5512 went to partition 1 and were read back in the order they were written, while booking B-5513 went to partition 2. The reader learned each event's type from the header, without parsing the JSON value.
The Event, Redesigned
Here is the original event, rebuilt with each decision applied. It is keyed by B-5512, and the headers carry event-type: RoomBooked and event-id: e-90a1. The dates are shortened to month and day here; a real event carries the full date.
{"eventId":"e-90a1","bookingId":"B-5512","hotelId":"H-204", "guestId":"G-31877","roomType":"double","checkIn":"03-14","checkOut":"03-17", "nights":3,"totalCents":53700,"currency":"USD","occurredAtMs":1741616520000}
The price is an integer number of cents, because a decimal amount such as 537.10 becomes an inexact floating-point number in many languages, and different languages can round it differently.
Using This in an Interview
When you draw an event in a design, name it and state its key: "The booking service publishes RoomBooked and BookingCancelled, keyed by booking ID so each booking's events stay in order. They carry the facts of the booking but only the guest ID, so personal data stays in the guest service."
Two follow-up questions are common:
- "Why not publish just the booking ID?" At 50 bookings a second with four readers, that is 200 extra calls a second to the booking service, and a reader that is behind would read the current state rather than the state at booking time.
- "How does a reader handle the same event twice?" Every event carries a unique event ID, so a reader records the IDs it has handled and skips any it has already seen.
Key Takeaways
- Name each event as one business fact in the past tense, such as
RoomBooked, rather than a generic update. - Give every event a unique event ID, the ID of what it describes, and an unambiguous time.
- Key by the thing whose events must stay in order; in the real run, all three events keyed
B-5512landed in one partition, in order. - Thin events add calls back to the source and can read a newer state; full events add size and copy data into every reader.
- Carry the facts of the change plus identifiers for everything else, and keep personal data out of events where an ID will do.
- Headers carry routing values such as the event type and event ID, so readers need not parse the value.
A well-designed event is still a cost, and the costs in the earlier lessons apply to every event a system publishes. The next lesson, When Not to Use Events, turns those costs into a short test for deciding when a request, a database transaction, or a plain queue is the better choice.
Practice Questions
Try each question first, then open the answer.
1. A team proposes one event type, OrderChanged, for every change to an order. What does this cost each reader, and what is the alternative?
Every reader must read every change and work out what happened. A shipping service that cares only about payment would still receive address edits and notes, and compare fields to find the payment. Separate events such as OrderPaid, OrderShipped, and OrderCancelled let each reader ignore the types it does not need.
2. A ride-sharing service publishes TripCompleted thin events, carrying only the trip ID, at 120 trips a second. Five services read the topic, and each needs the fare and distance. How many extra requests a second does the trip service receive?
600 requests a second. Each of the five readers calls back once per event: 120 x 5 = 600. Adding the fare and distance to the event would remove all of them.
</details>3. Events for one bank account must be processed in the order they were written. Which key should they carry, and what happens if they carry no key?
<details> <summary>Show answer</summary>The account ID. Events with the same key go to the same partition, and a partition is read in order, so every event for one account keeps its order. With no key, events for one account can be written to different partitions, and a withdrawal could be processed before the deposit that came first.
</details>4. A full event is 1.2 KB, and the topic receives 30 events a second, kept for 7 days with three copies. About how much disk does it use, and how does that change if the event is trimmed to 400 bytes?
<details> <summary>Show answer</summary>About 65 GB, falling to about 22 GB. 30 events a second is 2,592,000 a day. At 1.2 KB that is about 3.1 GB a day, and 7 days x 3 copies makes about 65 GB. At 400 bytes it is about 1.04 GB a day, or about 22 GB.
</details>5. A GuestRegistered event carries the guest's name, email, phone number, and passport number, and the topic keeps events for 30 days. Six services read it. What is the risk, and what should change?
Sensitive personal data is copied into six services and kept in the topic for 30 days. Each copy must be protected, and deleting a guest's data means finding every one of them. The event should carry the guest ID and only the fields its readers truly need, and the passport number should stay in the guest service.
</details>Reading Progress
0%
On This Page
Naming an Event
Identifiers and Time
Choosing the Key
Thin Events and Full Events
Headers
The Event, Redesigned
Using This in an Interview
Key Takeaways
Practice Questions