Real-time entity resolution for financial crime detection
PyFlink Β· Kafka Β· Feast Β· RocksDB Β· Union-Find streaming graph
Financial fraud rings consist of interconnected entities β accounts, merchants, devices, IP addresses, and cards β that collectively mask illicit activity. Detecting these rings in real time requires maintaining a continuously evolving graph of entity relationships.
StreamGraph solves this with an incremental Union-Find implemented as a
Flink KeyedProcessFunction whose state lives in RocksDB. Every payment
transaction produces entity edges; the Union-Find merges them into connected
components on the fly. When a component grows beyond a threshold β indicating a
potential fraud ring β a composite risk score is computed (with features from a
Feast online store) and a FraudAlert is emitted to Kafka.
Core innovations:
- π Streaming Union-Find with path-halving compression and union-by-rank,
backed by RocksDB
MapStatefor fault-tolerant, O(Ξ±(n)) entity resolution - β‘ Sub-millisecond graph updates at 100k+ edges/second on commodity hardware
- π§ͺ Realistic fraud ring generator producing star, chain, cycle, and dense ring topologies mixed into a realistic transaction stream
- πͺ Feast integration for Dual-store (offline Parquet + online Redis) feature management
- π¦ One-command local dev with Docker Compose (Kafka, Flink, Redis, Grafana)
Kafka (raw-transactions)
β
βΌ
TransactionFlatMap Parse JSON β EntityEdge (accountβmerchant, accountβdevice, β¦)
β
βΌ
DeduplicationFunction Drop duplicates within 24 h window (RocksDB ValueState)
β
βΌ
EntityResolutionFunction Incremental Union-Find keyed by shard_id (RocksDB MapState)
β β emits ComponentSnapshot on every merge
β
ββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββ
βΌ βΌ
Kafka (entity-components) FeatureEnrichmentFunction (Feast Redis lookup)
β
βΌ
RiskScorerFunction (8 weighted rules)
β
βΌ
AlertGeneratorFunction (threshold + rate limiter)
β
βΌ
Kafka (fraud-alerts)
See docs/architecture.md for the full design including sharding strategy, state layout, and scaling considerations.
- Docker β₯ 24 and Docker Compose β₯ 2.20
- Python β₯ 3.9 (for running tests and the generator locally)
make(GNU Make β₯ 4)
git clone https://github.com/your-org/streamgraph.git
cd streamgraph
cp .env.example .envmake upThis starts:
| Service | URL / Port | Description |
|---|---|---|
| Kafka UI | http://localhost:8080 | Browse topics and messages |
| Flink UI | http://localhost:8081 | Job graph, checkpoints, metrics |
| Grafana | http://localhost:3000 | Dashboards (admin/streamgraph) |
| Prometheus | http://localhost:9090 | Raw metrics |
| Redis | localhost:6379 | Feast online store |
The generator container immediately begins producing synthetic transactions
(~300 events/s, 8 % fraud ring traffic).
make dev-installmake test # full test suite with coverage
make test-unit # unit tests only (no Flink runtime needed)make benchmarkExample output:
StreamGraph Pipeline Benchmarks
==================================================
Entity Resolution (Union-Find) Throughput
N txns/s
10,000 312,450
100,000 298,130
500,000 285,980
Risk Scoring Throughput
N scores/s
10,000 924,100
100,000 901,220
500,000 887,500
streamgraph/
βββ src/streamgraph/
β βββ domain/
β β βββ models.py # Pydantic domain models (Transaction, EntityEdge,
β β # ComponentSnapshot, RiskScore, FraudAlert)
β βββ graph/
β β βββ union_find.py # LocalUnionFind (tests) + FlinkUnionFind (RocksDB)
β βββ operators/
β β βββ entity_resolution.py # EntityResolutionFunction (streaming Union-Find)
β β βββ deduplication.py # DeduplicationFunction (24 h window)
β β βββ feature_enrichment.py # FeatureEnrichmentFunction (Feast lookup)
β β βββ risk_scorer.py # RiskScorerFunction + compute_risk_score()
β β βββ alert_sink.py # AlertGeneratorFunction (threshold + rate limiter)
β βββ connectors/
β β βββ kafka_source.py # KafkaSource builder
β β βββ kafka_sink.py # KafkaSink builder
β βββ pipeline/
β β βββ fraud_detection.py # Full pipeline topology + CLI entry point
β βββ config.py # Pydantic-settings configuration
β
βββ generator/
β βββ fraud_ring_generator.py # Synthetic fraud ring data generator
β
βββ feast/
β βββ feature_store.yaml # Feast repository config
β βββ entities.py # Entity definitions
β βββ feature_views.py # FeatureView definitions
β βββ feature_services.py # FeatureService definitions
β
βββ tests/
β βββ unit/
β βββ test_union_find.py # 20+ unit tests + Hypothesis property tests
β βββ test_risk_scorer.py # Rule coverage + monotonicity tests
β βββ test_domain_models.py # Model validation + edge extraction
β βββ test_generator.py # Generator correctness
β
βββ benchmarks/
β βββ bench_union_find.py # pytest-benchmark + standalone runner
β βββ bench_pipeline.py # End-to-end throughput benchmark
β
βββ docs/
β βββ architecture.md # Detailed architecture document
β
βββ docker-compose.yml
βββ Makefile
βββ pyproject.toml
βββ .env.example
The heart of StreamGraph is FlinkUnionFind β a Union-Find whose backing
store is Flink MapState (RocksDB in production):
# In EntityResolutionFunction.process_element():
new_root, merged = self._uf.union(
edge.source_id,
edge.target_id,
transaction_id=edge.transaction_id,
)
if merged:
snapshot = self._build_snapshot(new_root, edge.timestamp)
out.collect(snapshot.model_dump_json())Path-halving compression (not full compression) is used because it achieves the same amortised O(Ξ±(n)) complexity but requires only one traversal pass β critical when each step is a RocksDB read:
def find(self, x: str) -> str:
while True:
parent = self._get_parent(x)
if parent == x:
return x
grandparent = self._get_parent(parent)
if grandparent == parent:
return parent
self._parent.put(x, grandparent) # path halving: skip over parent
x = grandparentThe operator is keyed by shard_id = blake2b(min(a, b)) % parallelism.
Using min(a, b) ensures that the edge (A, B) always routes to the same
shard as (B, A), preventing split-brain. Cross-shard merges emit a side
output to a merge coordinator (included as a skeleton; see
Issue #12 for the
distributed merge protocol).
The generator produces a realistic mixture of legitimate and fraudulent traffic:
# Preview 20 synthetic transactions
make generate
# Stream to Kafka at 500 events/s
python -m generator.fraud_ring_generator \
--kafka-bootstrap localhost:9092 \
--events-per-second 500 \
--num-rings 30 \
--fraud-ratio 0.10| Pattern | Description | Typical size |
|---|---|---|
star |
Hub account β many mule accounts | 8β25 |
chain |
A β B β C β D layering pattern | 5β15 |
cycle |
Circular money flow to obscure origin | 4β12 |
dense |
Near-clique; card testing micro-transactions | 4β12 |
Each ring type produces realistic transaction amounts, timing patterns, and shared infrastructure (device IDs, IP addresses) that match real fraud ring signatures.
| Feature View | Entity | TTL | Key features |
|---|---|---|---|
account_txn_stats |
account | 30 d | velocity, amount stats, geo diversity |
account_risk_scores |
account | 7 d | ML model scores (card-testing, mule, ATO) |
merchant_stats |
merchant | 30 d | chargeback ratio, MCC, risk category |
device_stats |
device | 90 d | linked accounts count, emulator flag |
ip_stats |
ip | 14 d | VPN/Tor/datacenter flags, abuse score |
To apply feature definitions and materialise to Redis:
make feast-apply
make feast-materialize| Rule | Weight | Description |
|---|---|---|
| R01 large_component | 30 % | Component size β₯ 4 (ring indicator) |
| R02 high_velocity | 20 % | >10 transactions in 1 hour |
| R03 amount_anomaly | 15 % | Amount z-score > 2.5Ο from 30-day mean |
| R04 card_testing | 15 % | Micro-transaction burst pattern |
| R05 cross_border | 5 % | >70 % cross-border transactions |
| R06 night_activity | 5 % | >70 % activity between 23:00β05:00 UTC |
| R07 device_sharing | 5 % | Device linked to β₯5 distinct accounts |
| R08 new_account_burst | 5 % | New account + high mule/velocity score |
Composite score = weighted sum, clipped to [0, 1]. Alerts are emitted for scores β₯ 0.35, with severity tiers at 0.50 / 0.75 / 0.90.
All settings are driven by environment variables (see .env.example).
Key variables:
| Variable | Default | Description |
|---|---|---|
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
Kafka broker list |
FLINK_PARALLELISM |
4 |
Task parallelism |
FLINK_CHECKPOINT_DIR |
file:///tmp/β¦ |
Checkpoint storage path |
FEAST_REDIS_HOST |
localhost |
Redis host for online store |
ALERT_SCORE_THRESHOLD_HIGH |
0.75 |
HIGH alert threshold |
GENERATOR_EVENTS_PER_SECOND |
200 |
Generator emission rate |
GENERATOR_FRAUD_RING_RATIO |
0.08 |
Fraction of fraudulent txns |
# 1. Start Kafka and Redis locally (or use docker-compose up kafka redis)
# 2. Create Kafka topics
./scripts/setup_kafka_topics.sh localhost:9092
# 3. Apply Feast features
make feast-apply
# 4. Start the generator
python -m generator.fraud_ring_generator \
--kafka-bootstrap localhost:9092 \
--events-per-second 200 &
# 5. Submit the Flink pipeline (requires a running Flink cluster)
python -m streamgraph.pipeline.fraud_detection \
--parallelism 4 \
--checkpoint-dir file:///tmp/sg-checkpointsContributions are welcome! Priority areas:
- Distributed merge protocol for cross-shard Union-Find merges (#12)
- Flink Async I/O for non-blocking Feast lookups (#15)
- Graph pruning job to evict stale nodes from RocksDB state (#18)
- XGBoost model integration replacing/augmenting rule-based scoring (#21)
- Kubernetes Helm chart for production deployment (#25)
- Avro schema registry integration for typed Kafka messages (#28)
Please open an issue before submitting a large PR. Run make lint typecheck test
before submitting.
If you use StreamGraph in research or production, please cite:
@software{streamgraph2024,
title = {StreamGraph: Real-time Entity Resolution for Financial Crime Detection},
year = {2024},
url = {https://github.com/your-org/streamgraph},
note = {Apache License 2.0}
}Apache License 2.0 β see LICENSE for details.