Imagine that an order service publishes an event whenever a customer places an order. The reporting team uses it to calculate sales, the notification team uses it to send confirmations, and the warehouse team uses it to prepare shipments.
The event contains a field called amount. Reporting treats it as dollars. Notifications treats it as cents. The warehouse assumes that receiving the event means payment has succeeded, even though the producer only meant that the order service accepted the order.
Kafka can deliver every record correctly while all three applications disagree about what happened. Good event design prevents that kind of failure. In this chapter, we’ll build an order event with a clear business meaning, well-defined fields, and enough context to remain useful through retries, delays, and replay.
An event contract is the agreement between a producer and its consumers about what an event means, when the producer publishes it, and how to interpret its fields. Its schema describes the data’s structure, but the contract also includes rules that field types alone cannot express.
Before choosing fields, write one sentence that defines the event. For our example:
OrderPlacedmeans the order service has durably accepted the customer’s order with the items and prices the service recorded when it accepted the order.
This definition establishes a specific boundary. The customer submitting a request is not enough: validation might fail. The event also does not imply that the payment service has captured payment, the inventory service has reserved stock, or shipping has started.
The following diagram separates that acceptance fact from later outcomes. It shows business milestones, not an implementation of reliable event publication.
A consumer can safely conclude that the order service accepted the order. Any stronger conclusion needs another fact or an explicit business rule.
The order service owns this event because it is responsible for accepting orders. A payment service can publish PaymentCaptured, but it should not independently invent OrderPlaced events based on what it thinks the order service did. Clear ownership gives consumers an authoritative source and gives teams someone to consult when the contract needs to change.
The order service must also coordinate saving the order with publishing its event. Publishing before a database transaction commits can announce an order that never exists; committing first and then crashing can leave an order without an event. A reliable publication mechanism must address that gap. Choosing a good event name does not solve it, and Kafka does not automatically make a database write and a Kafka write atomic.
Consider this intentionally flawed payload:
It is valid JSON, but it leaves basic questions unanswered. Does id identify the event or the order? What changed? What does status 2 mean? Which currency and unit does amount use? Which time zone applies to the timestamp, and what happened at that time?
Names such as OrderPlaced, OrderCancelled, and ShippingAddressChanged describe recognizable business facts. A generic OrderUpdated can be appropriate for a documented stream of order snapshots, but it is a poor substitute for deciding what consumers should learn from the message.
Use a past-tense name for a completed fact. CapturePayment asks another application to perform work; PaymentCaptured reports an outcome. Mixing those meanings makes it unclear whether a recipient should act or merely record something that already happened.
Prefer one meaningful occurrence per event. A single checkout operation may eventually produce several events, but an event called OrderPlacedAndPaymentCapturedAndEmailSent joins outcomes that can fail independently. It is difficult to publish honestly when only some of them succeed.
The right boundary follows the business. An order containing three line items can still be one OrderPlaced event because the service accepted the order as a whole. There is no need to split it into one event per field or item unless those pieces represent independent occurrences.
For this example, the producer writes to orders.placed with the Kafka record key ord-1042. The record value contains the following JSON. JSON makes the example easy to inspect; the design does not depend on that serialization format.
The outer fields form an event envelope: context that event types share. The data object contains the business details specific to this occurrence. This is an application convention, not a structure Kafka requires.
Here, source is a stable logical name for the system that owns the fact. It should not change whenever a container restarts or a hostname changes. Infrastructure details belong in operational telemetry unless they are part of the event’s meaning.
The diagram shows how this application event fits inside a Kafka record. A partition and offset identify where Kafka stores the record; they are not fields the application must add to its event value.
Keeping essential meaning in the value makes the event understandable when someone exports or inspects it outside Kafka. Headers are useful for metadata such as distributed tracing context, which connects work across services. If your contract places required information in headers, every forwarding and export path must preserve them.
Avoid maintaining competing copies of the same metadata without a rule for keeping them consistent. In this example, the record key must equal data.orderId. The producer should validate that rule before sending.
A field name helps, but it rarely provides the whole contract. For the order payload, the following rules remove ambiguity:
For simplicity, this contract covers orders without discounts. A system that supports discounts must define how they affect unit prices and totals rather than expecting consumers to reconstruct that policy.
All amounts here are integers in the currency’s minor unit. For USD, 129900 means $1,299.00. Do not assume every currency has two decimal places. The contract must define the currency code system and minor-unit convention, and supported values must fit the numeric types producers and consumers use.
The sample is internally consistent: two items at 59900 produce an items total of 119800; adding 2100 shipping and 8000 tax produces 129900.
Define what missing values mean. Suppose you add an optional deliveryInstructions field. If omission means “the customer provided no instructions,” document that meaning. Do not let one producer use omission for “unknown” while another uses it for “not applicable.” If those states affect processing differently, represent them explicitly.
Keep identifiers as identifiers. Consumers should not infer creation dates, locations, or customer categories from the shape of orderId unless that structure is explicitly part of the contract. Otherwise, a harmless ID-generation change can break downstream behavior.
An order can have many events. orderId connects them to the same business entity; eventId distinguishes the individual occurrences.
Define the uniqueness scope of eventId. In this example, the order service guarantees uniqueness within its stable source, so consumers that combine sources use (source, eventId) as the event identity. We shorten IDs such as evt-7001 for readability; production ID generation must meet that uniqueness rule.
When you retry publishing an existing event, preserve its identity and business payload. If a customer or service later cancels the order, create a new event with a new identity. Generating a fresh event ID for every publication attempt makes the same fact look like several different occurrences.
The diagram illustrates an application submitting the same event again after a send whose outcome the application could not confirm. Assume both writes reach partition 0, with an unrelated record occupying offset 43. This is an application-level resubmission, not an illustration of duplicate writes from an idempotent producer’s internal retry.
There are two stored records but one business occurrence. A topic, partition, and offset identify a Kafka record’s location; they cannot identify the same event after an application copies it to another topic or publishes it again at a new offset.
A stable event ID enables duplicate detection. It does not perform that detection or make a consumer’s database update atomic. The consumer still needs a processing strategy that coordinates its duplicate check with its side effect. Likewise, an idempotent producer does not enforce uniqueness based on the application’s eventId field.
For this example, ord-1042 is the record key because the application processes events for each order together. A key is a routing input, not a uniqueness constraint: Kafka can store many records with the same key. Ordering applies within a partition, and the application must ensure the records that require a shared order reach that partition and that consumers process them in the required order.
The order service may accept an order at 09:30 and publish its event at 09:31 after an outage. A lagging consumer may process the event at 09:35. These times answer different questions.
Our occurredAt field records when the order service accepted the order. Its format includes a time zone; the Z in the example means UTC. Republishing the event at 09:31 must not change occurredAt to 09:31.
Kafka also has a record timestamp. With CreateTime, Kafka uses the producer-provided timestamp; with LogAppendTime, the broker replaces it with append time. A producer can deliberately set the record timestamp from the event’s occurrence time, but consumers should not assume that mapping without an agreement. The explicit occurredAt field keeps the business meaning available independently.
Timestamps do not establish a reliable sequence across producers. Clocks can differ, and a producer may publish late. If consumers must detect stale changes to an entity, an application-defined entity revision can help, provided the owning service assigns it consistently. A revision is separate from both an event ID and a schema version; adding a number alone does not enforce processing order.
Historical values matter just as much as historical time. Suppose the product’s current price rises from $599 to $649 a month after this order. Replaying OrderPlaced should still reconstruct the accepted $599 unit price.
If the event only contains product IDs and the consumer fetches today’s prices, rebuilding yesterday’s sales can produce different results. Include the values needed to interpret the original fact, or use references to historical versions that remain retrievable for the required replay period.
An event is an immutable statement about an occurrence. Cancelling an order does not make the original acceptance untrue. Publish a new cancellation event. If the original event contained an error, publish an explicit correction that identifies what it corrects and defines how consumers should apply it. Reusing an event ID with different business data leaves consumers unable to distinguish a duplicate from a correction.
A useful event contains enough data for its intended consumers to interpret the fact. It does not need every field from the producer’s database.
The sample includes accepted prices because reporting needs historical amounts. It includes product IDs and quantities because they describe what the customer ordered. It leaves out database lock fields, internal processing flags, and the customer’s entire profile because those details do not explain order acceptance.
Copying an internal database row directly into a public event also couples consumers to storage decisions. Renaming a column or reorganizing tables can become a contract change. For a business event, map internal data into a deliberate public structure. A stream that describes database changes has a different purpose. Document that purpose explicitly.
There is a trade-off between including data and fetching it later. An event carrying only orderId can be appropriate when its purpose is to notify consumers to retrieve current order state. That contract accepts a dependency on the order API and does not promise a historical snapshot. The payload here serves a different need: describing the order at acceptance time.
Keep sensitive data limited to what the event’s audience requires. A broad reporting stream should not automatically carry delivery addresses, payment credentials, or access tokens. Even customer identifiers can be sensitive when someone links them to other records, so access and retention still need attention.
Large attachments usually belong in dedicated storage. An event can carry an object identifier, version, and integrity information when needed. That reference creates its own contract: authorized consumers must be able to retrieve the same object for as long as the event promises useful replay. A link that expires tomorrow is insufficient for a consumer rebuilding data next month.
An event becomes harder to change once independent consumers depend on it. Review both its structure and its meaning before making it available.
For OrderPlaced, the producer should validate that required identifiers are present, quantities are positive integers, monetary fields use the agreed representation, totals reconcile, and the record key matches orderId. Checking that the payload is valid JSON cannot catch all of those mistakes. Kafka brokers do not enforce these application-specific business rules.
A practical contract review should answer:
The contract should live with the event definition and include a representative example such as the one above. A new consumer team should be able to understand it without reading the producer’s database schema or discovering hidden assumptions in another consumer’s code.
Treat changes in meaning as carefully as changes in structure. Reinterpreting totalMinorUnits to exclude tax can break reporting even though it remains an integer and every message still parses. Reliable event design depends on keeping the promise behind the fields stable.
A good Kafka event describes a clear business fact that a specific system owns. Its fields make identity, occurrence time, units, and historical context explicit, so consumers can interpret it consistently.
Keep event identity separate from entity identity and Kafka offsets. Preserve the event’s identity and meaning through retries, represent new outcomes with new events, and include the context consumers need without copying unnecessary data. Kafka stores and transports the record; applications define and uphold its business contract.