AlgoMaster Logo

Building a Producer

Medium Priority16 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

Publishing an event from an application involves more than calling send(). The application needs to construct a useful record, connect to Kafka, check whether delivery succeeded, and keep the producer alive until its work is finished.

In this chapter, we’ll build a small Java program that publishes three order events. We’ll run it against a local Kafka broker, inspect the stored records, and see how the program behaves when it cannot connect. The goal is a complete, understandable producer with explicit success and failure handling.

1. Preparing the Environment

We’ll use Java 21, Maven 3.9 or newer, and the Apache Kafka Java client 4.3.1. Maven downloads the client library and its dependencies, so the first build requires internet access.

Run the Java application directly on your host machine. Kafka should be running in a Docker container named kafka-local, using apache/kafka:4.3.1, with Docker mapping host address 127.0.0.1:9092 to container port 9092. The broker should advertise localhost:9092. This is a single-node KRaft setup with plaintext connections and no authentication, intended only for local learning with sample data.

Check the host tools:

Both commands should report Java 21 or a compatible newer JDK. Maven can use a different Java installation from the one your terminal finds, so check its output too.

If the learning container already exists but is stopped, start it:

If you do not have that container, create it with the local port mapping:

Use only the command appropriate to your situation. Once Kafka has started, check that it responds:

Create a dedicated topic so earlier experiments do not affect the expected records:

If that topic already exists, choose a fresh name and use it in both the Java program and all Kafka commands below. Do not delete an existing topic just to make its offsets match the example.

One partition makes the output predictable. One replica is necessary for this single-broker setup, but it provides no backup copy. Keep other producers from writing to the topic during the walkthrough.

The diagram shows where each tool runs:

The Java program connects from the host, while docker exec runs Kafka’s console tools inside the container. Running the Java program in another container would require a different listener setup because localhost would refer to that other container.

2. Creating the Java Project

Create a project directory and the package directory for the Java source:

Run the Maven commands in this directory. Create a file named pom.xml with the following contents:

The Kafka dependency provides the producer API. slf4j-simple supplies a basic logging implementation compatible with this client’s logging dependency, so connection warnings remain visible. The compiler plugin selects the Java language level, and the exec plugin lets us run the application through Maven.

There is no application framework here. Keeping the project small makes it easier to see which responsibilities belong to the producer and which belong to our code.

3. Defining the Events and Configuration

Each event will have a Kafka key identifying its order and a JSON value describing the order placement. The event ID identifies the particular event; the order ID identifies the order it concerns.

We’ll publish these fixed sample events:

Scroll
Event IDKafka key and order IDAmount in paiseAmount in INR
evt-7001ord-10421299001,299.00
evt-7002ord-104349900499.00
evt-7003ord-10442599002,599.00

All three use the fixed sample occurrence time 2026-09-12T09:30:00Z. We’ll also attach a source header containing the UTF-8 bytes of order-service.

Both key and value are Java strings, so we’ll configure StringSerializer for each. It converts strings to bytes; it does not turn an arbitrary Java object into JSON or validate our event fields.

The producer uses acks=all and explicitly enables idempotence. These settings make successful sends wait for the required acknowledgment and protect against duplicates caused by the producer’s internal retries. They do not prevent a second application run from publishing the same events again. With one replica, an acknowledgment still confirms only one broker’s copy.

For a short local failure demonstration, we’ll use a five-second metadata/buffer wait, a five-second request timeout, and a fifteen-second delivery timeout. These values make failures observable without a long wait. They are learning settings, not production recommendations, and they do not impose a fifteen-second deadline on the entire program.

4. Implementing the Producer

Create src/main/java/com/example/kafka/OrderProducer.java with this complete program:

The OrderEvent record holds our sample fields. Its json() method uses a Java text block to keep the small fixture readable. This formatting is safe for these fixed identifiers. For arbitrary application input, use a JSON library that correctly escapes strings; interpolating customer input into JSON this way can produce invalid data.

The ProducerRecord constructor receives the topic, key, and value. We do not specify a partition or offset. The client selects the partition, and the broker assigns the offset. Since the topic has one partition, all three records go to partition 0.

The client.id value identifies this producer in logs and metrics. It is not a consumer group name, an authentication identity, or a key for deduplicating events.

5. Tracking Delivery Results

The program has two loops for a reason. The first submits records and keeps their futures. A future lets the application wait for an operation’s result. The second loop waits for each result and reports its outcome.

Calling get() immediately after every send() would wait for one record before submitting the next. Here, already-submitted records can make progress together. We deliberately limit the future list to three events; retaining a future for every event in an unbounded stream would eventually exhaust application memory.

The diagram shows how the program accounts for both ways a send can fail:

Some problems cause send() itself to throw. Others appear through a failed future, where get() throws ExecutionException and getCause() provides the underlying error. The program handles both and continues accounting for the remaining events in this finite sample. It does not treat a failure as permission to resend.

An interruption is different: it means another part of the program has asked the thread to stop waiting. The program preserves the interrupt signal and exits through an exception rather than printing a success message. Interruption does not prove that unfinished writes failed to reach Kafka.

The try-with-resources statement closes the producer when the block exits, including on an exception. During normal execution, every future has already reached a result before closing. There is no need for a separate flush() after those waits.

This program checks futures on its main thread to make the results easy to follow. A service can instead use callbacks to process completion notifications, but those callbacks should be brief because they generally run on the producer’s I/O thread. Whichever mechanism you use, successful method return alone is not enough to report delivery success.

6. Running and Verifying the Producer

Build and run the application from the directory containing pom.xml:

The first run downloads dependencies and may take longer. Kafka client logs and Maven output will appear alongside the program’s messages.

On a fresh topic, with no other producer writing to it, expect these confirmations:

The program prints results in the order it checks the futures. In this example, one thread also submits the events to one partition in that order, with idempotence enabled. Do not generalize the printed order into an ordering guarantee across multiple partitions.

Now read the stored records using the console consumer:

This reads partition 0 directly without saving consumer-group progress. The consumer exits after three records, or times out if it waits too long for more data. A timeout is not evidence that the consumer read all three records.

The first record should look like this:

The next two records should contain evt-7002 at offset 1 and evt-7003 at offset 2, with the keys and amounts from our sample data. The formatter does not display headers by default. To inspect the attached header, rerun the consumer command with the additional option --formatter-property print.headers=true.

These checks confirm two different things. The producer’s result confirms that Kafka satisfied the configured write acknowledgment. The console output shows that a consumer can read the record. Neither demonstrates that a consumer has fulfilled an order or sent an email, or another application has committed its processing progress.

7. Observing a Failed Connection

The program accepts an optional bootstrap address as its first argument. This lets us check its failure behavior without stopping the broker or changing the working setup.

Choose a local port with no service listening; the command below assumes 19092 is unused:

The producer should log connection warnings and fail to obtain topic metadata. After the configured waits, the program reports failures and Maven reports an unsuccessful execution. The exact diagnostic text can vary, but you should not see All 3 events confirmed.

The three sends can each spend time waiting for metadata, so do not expect the entire run to finish after exactly five seconds. The program keeps trying the remaining fixture events to show their individual outcomes. A service encountering a persistent outage would usually stop accepting more publishing work or apply backpressure rather than continue an unlimited loop.

This particular test points to an unused port from the start, so none of its records can reach the intended broker. A timeout during a real connection failure is less conclusive: the broker may have stored a record before the producer lost its response.

The distinction matters when the producer confirms only some events. A failed overall run does not roll back successful writes. Our three sends are independent, non-transactional operations.

Before returning to the working address, remember that another successful run publishes the three sample events again. On an otherwise unchanged topic, a second successful run adds them at offsets 3, 4, and 5. The repeated event IDs do not make Kafka discard them.

To inspect those new records, use the same consumer command with --offset 3. Reading from offset 0 with --max-messages 3 would only show the original records again.

8. Producer Ownership and Troubleshooting

This is a short-lived command-line program, so its main method owns the producer and closes it after checking the results. A long-running service should similarly assign clear ownership, but usually reuse one producer across many events. The Java producer is thread-safe; creating a new instance for each request wastes connections, buffers, and setup work.

During service shutdown, stop new submissions, account for outstanding sends, and then close the producer. A service may use a bounded close timeout to fit its shutdown deadline. If that deadline expires, unresolved events still need a recovery policy. Closing the client does not create durable storage for records that remained only in memory.

The same responsibility applies before publishing. This sample constructs events in memory and does not update an order database. In a real order service, saving an order and sending its event are separate operations. The application can crash between those operations. To recover, it must store the pending event durably and have a way to publish it later.

If the walkthrough does not behave as expected, narrow the problem down by its stage:

SymptomWhat to check
Maven reports an unsupported Java releaseCheck the JDK shown by mvn -version; the project targets Java 21.
Maven cannot resolve dependenciesCheck internet access, repository access, and any Maven proxy settings.
Java cannot find OrderProducerCheck the package, source path, and that you ran mvn compile exec:java from the project directory.
Kafka tools work inside Docker, but Java cannot connectCheck the host port mapping and the broker’s advertised address.
Metadata lookup times outCheck the bootstrap address, broker readiness, and exact topic name.
Offsets differ from 0, 1, and 2Check whether the topic had earlier records, another producer wrote to it, or the program ran more than once.
Maven reports failure after some confirmationsSome sends may have succeeded. Inspect those results before rerunning the program.
Consumer shows only earlier eventsCheck the requested start offset and --max-messages limit.

When finished, the Java process has already closed its producer. The console consumer exits after its record limit or timeout. You can leave Kafka running, or stop the learning container while retaining its current data:

Summary

You’ve built a Java producer that creates keyed order events, attaches a header, submits records, and checks every delivery result before reporting success. You’ve also read the stored records and observed a connection failure without disrupting the broker.

A usable producer needs both a publishing path and a way to account for failures. Keep its lifetime explicit, distinguish confirmed delivery from consumer processing, and inspect uncertain or partial outcomes before deciding to publish again.