ReviseAlgo Logo

Real-World System Design with Kafka

Real-Time Fraud Detection System with Kafka Streams

Correlating and analyzing payments streams using rolling windows to flag fraudulent transactions.

Introduction

Identifying fraudulent payments requires analyzing data streams in motion. Waiting for database batch runs is too slow; transactions must be evaluated in milliseconds. Real-Time Fraud Detection with Kafka Streams relies on sliding time windows and local state stores (RocksDB) to correlate event sequences and flag suspicious activities immediately.

Why It Matters

Fraud patterns often involve quick successions of transactions across geographical distances (e.g. Card swiped in Chicago and 2 minutes later in Paris). Analyzing these relationships requires retaining temporary state of recent events. By utilizing Kafka Streams' windowing features and stream-stream joins, systems identify these patterns dynamically without querying slow disk databases.

Real-World Analogy

Think of Kafka Streams windowing like a surveillance video loop. The security system keeps a running 5-minute recording buffer of the entrance door (sliding window). As new events arrive, they are compared with other events in the buffer. If the same person is seen entering two different buildings in the buffer window, the alarm goes off. Older video footage beyond the 5-minute mark is continuously discarded (purging window state) to save storage.

How It Works

The topology of a Kafka Streams fraud detector executes these steps:

  1. Stream Ingestion: A KStream reads transactions from the topic (e.g. card-transactions).
  2. Repartitioning by Card: The stream is keyed by card number, grouping all transactions for that card onto the same stream processor.
  3. Sliding Window aggregation: The topology defines a 5-minute sliding window, keeping track of the last known location/city.
  4. Stateful Join/Evaluation: If a new transaction location differs from the previous location in the state window, it calculates distance/time, raises a fraud alert, and routes it to an alert topic.

Internal Architecture

To achieve fast lookups without heap memory overhead, Kafka Streams uses RocksDB as an embedded key-value store on the host disk. When window aggregations are computed, state is written to RocksDB. To guarantee durability, changes to RocksDB are piped asynchronously to a hidden, compacted Kafka changelog topic. If a container crashes, the new task reads the changelog topic to rebuild the state in RocksDB.

Visual Explanation

Below is an ASCII representation showing transactions processed in a sliding window to detect geographical fraud:

 [Transaction: NY, 12:00] ──> [ KStream: transactions ] 
                                     │ (Grouped by Card ID)
                                     ▼
                      [ RocksDB State: Chicago, 11:58 ] ──> (Diff Locations < 5m)
                                     │
                                     ▼ (Fraud Alert Triggered)
                           [ KStream: fraud-alerts ]
            

Practical Example

Here is a Java example using the Kafka Streams DSL to define a topology checking for high-frequency transactions on the same card within a 1-minute window:

Common Mistakes

  • Not setting a grace period for late-arriving events: Omitting grace periods. Network lags can cause coordinates to arrive late. Without grace configurations, Kafka Streams drops late records, leading to false negatives.
  • Sizing window durations too large without disk capacity planning: Retaining days of sliding window data. RocksDB will exhaust host disk space, causing disk write failures and crashing stream tasks.

Quick Quiz

Q1: What is the purpose of the changelog topic in Kafka Streams stateful operations?

It durably backs up local RocksDB state modifications in Kafka, enabling new nodes to recover and rebuild local database cache on failover.

Q2: How does a sliding time window differ from a tumbling time window?

Tumbling windows are non-overlapping fixed blocks (e.g. 12:00-12:05). Sliding windows slide continuously over time, evaluating events relative to the arrival of each record.

Scenario-Based Challenge

The Challenge: Joining dynamic card limits against the transaction stream

You need to block transactions that exceed a card's daily spending limit. The daily limits are updated dynamically by users in their banking app and stored in a MySQL table. The transactions stream in constantly at 5,000 events/second. How do you design this join?

Solution Tip: Mirror the MySQL limits table into a Kafka topic using CDC (Debezium), and ingest it in Kafka Streams as a KTable (or GlobalKTable). Join the incoming transaction KStream with the limits KTable using a key-based join, verifying if the transaction amount + current day total exceeds the daily limit.

Debugging Exercise

During high traffic, your Kafka Streams app slows down, task lags grow, and host metrics show extremely high disk I/O wait times on the directory hosting RocksDB stores.

The Fix: RocksDB is executing heavy cache misses, forcing disk reads. Optimize performance by increasing the off-heap block cache size using a custom RocksDBConfigSetter, ensuring SSD storage is used instead of magnetic disks, and scaling tasks across more nodes to split partition memory pools.

Interview Questions

1. What is the difference between stream-stream joins and stream-table joins?

Stream-stream joins correlate two dynamic event streams within a specific time window (e.g. click + purchase). Stream-table joins enrich a dynamic stream with static or slow-changing metadata looked up from a key-value state (e.g. transaction + user info).

2. How are out-of-order events handled in stateful aggregations?

By defining a grace() period on windows. Kafka Streams retains window buffers open for the grace duration, allowing late events to update aggregations before final state emission.

Production Considerations

Configure RocksDB stores to reside on high-speed NVMe drives. Always enable metrics collection on task processing latencies and configure alert alerts on task thread crashes.