Implementing Stream Joins
1. Understanding Join Types
| Join | Left | Right |
|---|---|---|
| Stream-Stream | KStream | KStream (windowed) |
| Stream-Table | KStream | KTable (lookup) |
| Table-Table | KTable | KTable |
Example: Join overview
stream⋈stream = windowed; stream⋈table = enrichment; table⋈table = materialized
2. Using Stream-Stream Join
| Aspect | Description | Detail |
|---|---|---|
| Window Required | JoinWindows mandatory | Bounded state |
| Co-Partition | Same key + partitions | Required |
| Both Sides Stored | Window state stores | Buffer |
Example: Stream-stream join
clicks.join(impressions,
(c, i) -> merge(c, i),
JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(5)));
3. Using Stream-Table Join
| Aspect | Description | Detail |
|---|---|---|
| Lookup | Stream record looks up table | Enrichment |
| Trigger | Stream side only | Table is passive |
| No Window | Point-in-time lookup | Latest table value |
4. Using Table-Table Join
| Aspect | Description | Detail |
|---|---|---|
| Materialized | Result is a KTable | Changelog |
| Trigger | Either side update | Both active |
| Foreign Key | Non-key join supported | Since 2.4 |
5. Implementing Inner Join
| Aspect | Description | Detail |
|---|---|---|
| Output | Only when both match | Intersection |
| Method | join() | ValueJoiner |
| Null | No emit if either missing | Strict |
6. Implementing Left Join
| Aspect | Description | Detail |
|---|---|---|
| Output | Every left record | Right may be null |
| Method | leftJoin() | Handle null |
| Use | Optional enrichment | Default values |
Example: Left join with default
orders.leftJoin(customers,
(o, c) -> o.withName(c != null ? c.getName() : "UNKNOWN"));
7. Implementing Outer Join
| Aspect | Description | Detail |
|---|---|---|
| Output | Either side present | Union |
| Supported | Stream-stream, table-table | Not stream-table |
| Null | Missing side is null | Both nullable |
Example: Outer join
a.outerJoin(b,
(x, y) -> (x == null ? "-" : x) + "/" + (y == null ? "-" : y),
JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(5)));
8. Configuring Join Windows
| Config | Description | Detail |
|---|---|---|
| timeDifference | Max time gap | Symmetric |
| before/after | Asymmetric bounds | One-sided |
| grace | Late record tolerance | ofTimeDifferenceAndGrace |
Example: Asymmetric window
JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(5))
.before(Duration.ofMinutes(1));
9. Setting Join Retention
| Aspect | Description | Detail |
|---|---|---|
| Retention | How long window kept | ≥ window + grace |
| State Size | Bounded by retention | Memory/disk |
| StreamJoined | Configure store retention | Per join |
Example: Join store config
left.join(right, joiner, windows,
StreamJoined.with(Serdes.String(), Serdes.String(), Serdes.String()));
10. Understanding Join Semantics
| Aspect | Description | Detail |
|---|---|---|
| Co-Partitioning | Same partition count + key | Mandatory |
| Repartition | Auto if keys changed | selectKey |
| GlobalKTable | No co-partition needed | Full replica |
Example: GlobalKTable join
stream.join(globalTable,
(k, v) -> v.getCustomerId(),
(v, g) -> enrich(v, g));
11. Optimizing Join Performance
| Technique | Description | Benefit |
|---|---|---|
| GlobalKTable | Avoid repartition | Less shuffle |
| Smaller Windows | Less buffered state | Memory |
| Pre-Partition | Align keys upstream | No repartition |