Kafka-Streams Join 2 topics with JSON values | backpressure mechanism?
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
Introduction
Joining two Kafka topics in Kafka Streams is mostly about choosing the right key, the right join type, and the right JSON serde. The "backpressure" question is different: Kafka Streams does not expose reactive-streams style backpressure operators, but it does naturally slow intake through Kafka's pull-based consumer model and increasing lag.
First: Make the Join Key Correct
Kafka Streams joins happen on record keys, not on arbitrary fields inside the JSON payload. If your JSON values contain the business ID and the record key does not, you must re-key the stream before joining.
Assume one topic contains user profiles and another contains logins:
If both topics are keyed by userId, the join is straightforward. If not, re-map the key first.
Joining Two JSON Streams
A typical stream-stream join with JSON payloads looks like this:
This performs a stream-stream inner join, meaning each pair must have matching keys and timestamps that fall inside the join window.
Understand Which Join You Actually Need
There are three common join families in Kafka Streams:
- '
KStreamtoKStream: event-to-event join within a time window' - '
KStreamtoKTable: event joined against the latest table state' - '
KTabletoKTable: state-to-state join'
If one topic is really reference data such as user profile state, a KTable may be a better model than a second KStream.
That would look more like this:
This is often what people actually want when "joining two topics" where one topic is current entity state.
JSON Handling Strategy
Kafka Streams itself does not care that the payload started as JSON. What matters is that both topics are deserialized into stable Java types or JsonNode values before the join.
Strongly typed models are usually easier to maintain than generic JSON trees because:
- The join function becomes type-safe
- Schema drift is easier to detect
- Tests are easier to read
If your schema changes frequently, a JsonNode-based approach may still be useful, but it pushes more runtime validation into your topology code.
What "Backpressure" Means Here
Kafka Streams does not implement reactive pull signals between operators the way Reactor or RxJava might. Instead, it consumes records from Kafka partitions, processes them, commits progress, and naturally falls behind when it cannot keep up.
In practice, that means:
- The consumer polls at the pace the application can sustain
- Input lag grows if processing is slower than arrival rate
- Kafka retains records, so data is buffered in the broker rather than dropped immediately
That is why people often say Kafka uses "backpressure by lag." It is not a dedicated backpressure API, but it is a real flow-control effect of the pull-based model.
If the Join Falls Behind
When a join-heavy topology slows down, the usual causes are:
- Expensive JSON serialization or deserialization
- Too few stream threads
- Large state stores or slow disks
- Bad key distribution causing partition hotspots
- Join windows or grace periods that increase state-store pressure
The fix is usually operational or architectural, not "turn on backpressure." Tune partitioning, serialization, state stores, and thread count first.
Common Pitfalls
The biggest mistake is trying to join on a field inside the JSON value while leaving the Kafka record key unrelated. Kafka Streams joins on keys, so re-keying may be required before the join.
Another issue is choosing a stream-stream join when the second topic is really reference state. In those cases, a stream-table join is often simpler and more correct.
Developers also expect reactive-streams style backpressure controls from Kafka Streams. Kafka Streams handles overload differently: the app slows, lag grows, and Kafka buffers the backlog.
Finally, JSON serdes can become a hidden performance cost. If the topology is lagging, serialization overhead is worth profiling before blaming the join itself.
Summary
- Kafka Streams joins operate on record keys, so key design comes first.
- Use typed JSON serdes to keep joins readable and maintainable.
- Choose between stream-stream and stream-table joins based on the data model.
- Kafka Streams does not expose reactive backpressure operators; overload shows up as consumer lag.
- If a join topology falls behind, inspect keys, serdes, partitions, thread count, and state-store pressure.
Related reading
- Kafka-streams setting internal topics cleanup policy to delete doesn't work
- Kafka-topics --list using ssl
- kafka-topics.sh ---delete --topic ''testTopic'' is not working for kafka V 0.10.1
- Kafka-topics.sh authentication
- Kafka10.1 heartbeat.interval.ms, session.timeout.ms and max.poll.interval.ms
- Kafka - Best practices in case of slow processing consumer. How to achieve more parallelism?
- Kafka - Broker fails because all log dirs have failed
- Kafka - Broker Group coordinator not available

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.