In six months at N26 we replaced a static rule engine spread across dozens of microservices with a single stateful Apache Flink pipeline. Shipping a new fraud rule went from four weeks, or three to six months for anything that needed state, to about two weeks. A payment reaches a risk decision in roughly 52 milliseconds. This is how it worked and what it cost.
When I joined the Financial Crime Prevention team in late 2019, the fraud detection system was doing its job, barely. Static rules were scattered across dozens of microservices. Every new fraud vector meant a pull request, a review cycle, a deploy, and a prayer that the new rule didn’t break the old ones. Some rules we simply couldn’t write at all, because the architecture had no concept of state or time windows beyond what a single microservice could hold in memory.
None of that was an engineering failing. Those were architectural constraints, and they were costing us in fraud losses, false positives, and customer experience. N26 is a licensed EU bank with millions of active customers, which means every one of those costs is also a compliance exposure. We had six months to replace it.
The problem with static rules in microservices
Rule-based fraud detection made sense when N26 was smaller. You had a manageable transaction volume, a small team, and a fairly predictable set of fraud patterns. You encoded the patterns as static rules in a service, deployed it, done.
The problem is that fraud evolves faster than a microservice deploy cycle. By the time a fraud pattern was documented, triaged, written as a rule, tested across all the dependent services, and deployed, the attack vector had often shifted. You end up playing catch-up forever. And because each rule lived in its own service context, understanding the interaction between rules deployed across different teams and codebases was nearly impossible.
The deeper problem was state. Most fraud signals aren’t in a single transaction. They’re in the relationship between transactions: a customer who made three small purchases in Berlin, then a large ATM withdrawal in Lagos two hours later, then a card-not-present transaction in Singapore. No single event is suspicious in isolation. The pattern is.
Static rules in microservices are stateless by design. Each evaluates a single event in isolation. Building a rule that needed context (“has this customer’s transaction velocity in the last hour deviated from their 30-day baseline?”) required stitching together data across services, each with its own consistency model and latency profile. The result was rules that were either too simple to catch sophisticated fraud, or too slow to block it in time.
Why Apache Flink
We replaced the microservice rule sprawl with an event-driven risk scoring system built on Apache Flink. It gave us three things the old architecture couldn’t.
True stateful stream processing. Flink’s managed state, held in RocksDB backends and checkpointed to durable storage, meant a rule could read a customer’s full behavioural history without an external database round-trip. State lived inside the job, consistent and local.
Windowed aggregations as a first-class primitive. Tumbling, sliding, and session windows are built into Flink’s DataStream API. A rule that needed “count of transactions in the last hour grouped by device fingerprint” went from a multi-service engineering project to a few lines of windowed aggregation logic.
Exactly-once semantics. In fraud prevention, processing a transaction twice means blocking a legitimate payment, or missing a fraudulent one. Flink’s checkpoint-based exactly-once guarantees let us reason about correctness without building custom idempotency layers into every consumer.
The end-to-end shape is unremarkable: payments land on a Kafka topic, the Flink job scores each one between 0.0 and 1.0, and the score decides approve, step-up, or block. What matters is the budget between those steps.
The deployment model that changed everything
The old system’s bottleneck sat in the deployment pipeline rather than the rule logic. Each rule lived in its own microservice, which meant its own CI/CD, its own review process, its own coordination with dependent teams. Best case: four weeks. Typical case: months.
With Flink, rules are operator chains inside a single streaming job, and a new one moves through three stages. Week one is shadow mode: the rule processes live traffic and emits scores to a monitoring topic only, no customer impact, while compliance validates it against known fraud cases. Week two is canary: 5% of traffic, with alerts if the false positive rate spikes, ramping to 100% if it stays clean. Then it runs on full traffic, with state and windowing handled by the Flink runtime.
Two weeks, end to end. And because state and windowing became runtime concerns rather than engineering projects, we could write rules that were genuinely impossible in the old architecture: cross-border velocity checks, device fingerprint correlation over 30-day windows, session-based behavioural deviation scoring.
The part nobody talks about
Operational complexity turned out to be harder than the Flink topology. Flink jobs are stateful, which turns checkpoint sizing, state TTL, and savepoint migration into production concerns. A poorly sized RocksDB backend will degrade throughput silently. Consumer lag on the input topic compounds with checkpoint duration to create backpressure cascades. Budget at least 30% of your timeline for observability, and get comfortable with Flink’s metrics ecosystem before you write a single streaming operator.
What I’d do differently
We built the risk engine first and then added monitoring. That was backwards. Spend the first two weeks defining metrics, dashboards, and alerts — checkpoint duration, state size per operator, event latency at every boundary — before writing business logic.
If the Payments team changes the transaction topic schema without coordination, your Flink job fails to deserialize and your checkpoint is poisoned. Schema Registry with FORWARD_TRANSITIVE compatibility is non-negotiable.
Poison pill events will happen. Route unparseable events to a dead-letter topic via Flink side outputs and alert on throughput — never let them halt the main job.
Every stateful operator needs a TypeSerializer that survives schema evolution and a documented TTL policy. Practice savepoint → upgrade → restore before it's production-critical.
Real-time fraud prevention at scale is a stateful stream processing problem, not a static rules problem. Once you accept that, the architecture becomes obvious.
I gave a talk about this at KotlinConf 2024 in Copenhagen, covering the engineering choices, the Flink topology, and what we learned operating stateful streaming jobs in a regulated environment. Watch the session.
Questions
How long does it take to ship a new fraud rule with Apache Flink?
About two weeks, versus four weeks to six months in the previous microservice architecture. The rule spends week one in shadow mode against live traffic with no customer impact, week two as a 5% canary, then ramps to full production once the false positive rate holds.
How fast is a real-time fraud decision at N26?
Roughly 52 milliseconds from payment intent to decision. That covers Kafka publication, Flink deserialization, stateful enrichment from RocksDB-backed behavioural profiles, an async ML model score, and threshold evaluation.
Why replace static rules in microservices with stream processing?
Static rules are stateless by design, so each one evaluates a single event in isolation. Most fraud signals live in the relationship between transactions, which requires state and time windows. Flink provides both as runtime primitives, so a rule that needed cross-service data stitching becomes a few lines of windowed aggregation.
What is the hardest part of running Flink in production?
Operational complexity, not topology design. Checkpoint sizing, state TTL, and savepoint migration all become production concerns, an undersized RocksDB backend degrades throughput silently, and consumer lag compounds with checkpoint duration into backpressure cascades. Budget at least 30% of the timeline for observability.