Course: Kafka Streams 101

Hands On: Joins

3 min
Sophie Blee-GoldmanSoftware Engineer II (Course Presenter)
Bill BejeckIntegration Architect (Course Author)

Hands On: Joins

If you haven’t already, clone the course GitHub repository and load it into your favorite IDE or editor.

git clone
cd learn-kafka-courses/kafka-streams

The source code in this course is compatible with Java 11. Compile the source with ./gradlew build and follow along in the code.

This module’s code can be found in the source file java/io/confluent/developer/joins/

This exercise teaches you how to join two streams into a third stream, and then join that third stream with a table.

  1. Use a static helper method to get SerDes for your Avro records (in subsequent exercises, you'll abstract this into a static utility method, in the StreamsUtils class of the course repo):

    static <T extends SpecificRecord> SpecificAvroSerde<T> getSpecificAvroSerde(Map<String, Object> serdeConfig) {
        SpecificAvroSerde<T> specificAvroSerde = new SpecificAvroSerde<>();
        specificAvroSerde.configure(serdeConfig, false);
        return specificAvroSerde;
  2. Use a utility method to load the properties (you can refer to the StreamsUtils class within the exercise source code):

    Properties streamsProps = StreamsUtils.loadProperties();
  3. Get the input topic names and the output topic name from the properties:

    String streamOneInput = streamsProps.getProperty("stream_one.input.topic");
    String streamTwoInput = streamsProps.getProperty("stream_two.input.topic");
    String tableInput = streamsProps.getProperty("table.input.topic");
    String outputTopic = streamsProps.getProperty("joins.output.topic");
  4. Create a HashMap of the configurations:

    Map<String, Object> configMap = StreamsUtils.propertiesToMap(streamsProps);
  5. Then create the required SerDes for all streams and for the table:

    SpecificAvroSerde<ApplianceOrder> applianceSerde = getSpecificAvroSerde(configMap);
    SpecificAvroSerde<ElectronicOrder> electronicSerde = getSpecificAvroSerde(configMap);
    SpecificAvroSerde<CombinedOrder> combinedSerde = getSpecificAvroSerde(configMap);
    SpecificAvroSerde<User> userSerde = getSpecificAvroSerde(configMap);
  6. Create the ValueJoiner for the stream-table join:

    ValueJoiner<ApplianceOrder, ElectronicOrder, CombinedOrder> orderJoiner =
        (applianceOrder, electronicOrder) -> CombinedOrder.newBuilder()

The stream is a result of the preceding stream-stream join, but it's a left outer join, because the right-side record might not exist.

  1. Create the ApplianceOrder stream as well as the ElectronicOrder stream:

    KStream<String, ApplianceOrder> applianceStream =, Consumed.with(Serdes.String(), applianceSerde))
            .peek((key, value) -> System.out.println("Appliance stream incoming record key " + key + " value " + value));
    KStream<String, ElectronicOrder> electronicStream =, Consumed.with(Serdes.String(), electronicSerde))
            .peek((key, value) -> System.out.println("Electronic stream incoming record " + key + " value " + value));
  2. From here, create the User table:

    KTable<String, User> userTable = builder.table(tableInput, Materialized.with(Serdes.String(), userSerde));
  3. Now create the stream-stream join and call the join method on the applianceStream, the left side (or primary) stream in the join. Add the electronicStream as the right side (or secondary) stream in the join, and add the orderJoiner created before:

    KStream<String, CombinedOrder> combinedStream =
  4. Specify a JoinWindows configuration of 30 minutes (a right-side record must have timestamps within 30 minutes before or after the timestamp of the left side for a join result to occur):

  5. Add the StreamJoined configuration with SerDes for the key, left-side, and right-side objects, for the joined state stores:

    StreamJoined.with(Serdes.String(), applianceSerde, electronicSerde))
  6. Add a peek operator to view the results of the join:

    .peek((key, value) -> System.out.println("Stream-Stream Join record key " + key + " value " + value));
  7. Call the join method on the KStream that results from the join in previous steps, adding the userTable as the right side in the stream-table join. Then add enrichmentJoiner to add user information, if available. Add the Joined object with SerDes for the values of both sides of the join, add the peek operator to view the stream-table join results, and write the final join results to a topic:

                    Joined.with(Serdes.String(), combinedSerde, userSerde))
        .peek((key, value) -> System.out.println("Stream-Table Join record key " + key + " value " + value))
        .to(outputTopic, Produced.with(Serdes.String(), combinedSerde));
  8. Create the KafkaStreams object, and again use the TopicLoader helper class to create topics and produce exercise data:

    KafkaStreams kafkaStreams = new KafkaStreams(, streamsProps);
  9. Finally, start the Kafka Streams application:


Use the promo code STREAMS101 to get $101 of free Confluent Cloud usage

Be the first to get updates and new content

We will only share developer content and updates, including notifications when new content is added. We will never send you sales emails. 🙂 By subscribing, you understand we will process your personal information in accordance with our Privacy Statement.