Real-Time Fraud Detection: Why Flink Is Overkill for Most Teams
Most fraud detection problems — velocity checks, amount thresholds, geographic anomalies, pattern matching — can be solved entirely in SQL against a live transaction stream. You don't need a Flink cluster, custom Java operators, or a dedicated ML inference pipeline to catch 90% of fraud. A streaming database handles it faster, with a fraction of the operational burden.
The Real Cost of Flink-Based Fraud Detection
Apache Flink is a powerful tool. It is also a significant infrastructure commitment. To run a production fraud detection pipeline on Flink, you need a dedicated cluster, a team fluent in the DataStream API or Table API, checkpoint configuration, state backend tuning (usually RocksDB), and a serialization strategy for complex state objects.
That is before you write a single fraud rule.
For teams at Series A and B fintechs — or even larger companies with lean data engineering functions — this operational overhead is the bottleneck. Rules take weeks to deploy instead of hours. Engineers spend more time managing state backends than writing detection logic.
The real-time requirement is real. The Flink requirement is not.
What Fraud Detection Actually Needs
Strip away the complexity and most fraud detection pipelines do four things:
- Velocity checks — how many transactions has this card/user done in the last N minutes?
- Threshold alerts — is this amount unusually large compared to this user's history?
- Pattern matching — is this sequence of transactions a known bad pattern (e.g., small test charge followed by large charge)?
- Enrichment and scoring — join live transactions against user profiles, merchant risk scores, device fingerprints.
All four of these are set-based, windowed aggregation problems. SQL is exactly the right language for them.
The Streaming Database Approach
A streaming database ingests your Kafka transaction stream, maintains materialized views continuously, and lets you query fraud signals at sub-second latency using plain SQL.
RisingWave is a PostgreSQL-compatible streaming database built in Rust, open source under Apache 2.0, with storage backed by S3. You define your fraud logic as SQL views. The engine keeps them current automatically as new transactions arrive.
Step 1: Ingest the Transaction Stream
CREATE SOURCE transactions (
transaction_id VARCHAR,
user_id VARCHAR,
card_id VARCHAR,
merchant_id VARCHAR,
amount NUMERIC,
currency VARCHAR,
country VARCHAR,
device_id VARCHAR,
event_time TIMESTAMPTZ
)
WITH (
connector = 'kafka',
topic = 'transactions',
properties.bootstrap.server = 'kafka:9092',
scan.startup.mode = 'latest'
)
FORMAT PLAIN ENCODE JSON;
Step 2: Velocity Check — Transactions Per Card in Last 10 Minutes
High transaction frequency on a single card is one of the strongest fraud signals. Define it as a materialized view:
CREATE MATERIALIZED VIEW card_velocity_10m AS
SELECT
card_id,
COUNT(*) AS txn_count,
SUM(amount) AS total_amount,
MAX(event_time) AS last_seen,
COUNT(DISTINCT merchant_id) AS unique_merchants,
COUNT(DISTINCT country) AS unique_countries
FROM transactions
WHERE event_time > NOW() - INTERVAL '10 minutes'
GROUP BY card_id;
This view is maintained incrementally. Every new transaction updates the relevant card's row in microseconds.
Step 3: User Spending Baseline
Compare live spending against each user's historical average to catch amount anomalies:
CREATE MATERIALIZED VIEW user_baseline_7d AS
SELECT
user_id,
AVG(amount) AS avg_txn_amount,
STDDEV(amount) AS stddev_amount,
MAX(amount) AS max_txn_amount,
COUNT(*) AS txn_count_7d
FROM transactions
WHERE event_time > NOW() - INTERVAL '7 days'
GROUP BY user_id;
Step 4: Real-Time Fraud Signal View
Join live transactions against both signals to produce a scored event stream:
CREATE MATERIALIZED VIEW fraud_signals AS
SELECT
t.transaction_id,
t.user_id,
t.card_id,
t.amount,
t.country,
t.event_time,
v.txn_count AS velocity_10m,
v.unique_countries AS countries_10m,
b.avg_txn_amount,
b.stddev_amount,
-- Amount anomaly: more than 3 standard deviations above mean
CASE WHEN b.stddev_amount > 0
THEN (t.amount - b.avg_txn_amount) / b.stddev_amount
ELSE 0 END AS amount_z_score,
-- Flag conditions
(v.txn_count > 10) AS high_velocity,
(v.unique_countries > 2) AS multi_country,
(t.amount > b.avg_txn_amount + 3 * b.stddev_amount) AS amount_spike
FROM transactions t
LEFT JOIN card_velocity_10m v ON t.card_id = v.card_id
LEFT JOIN user_baseline_7d b ON t.user_id = b.user_id;
Step 5: Sink Flagged Transactions to Your Alert System
CREATE SINK fraud_alerts_sink
FROM fraud_signals
WHERE high_velocity OR multi_country OR amount_spike
WITH (
connector = 'kafka',
topic = 'fraud-alerts',
properties.bootstrap.server = 'kafka:9092'
)
FORMAT PLAIN ENCODE JSON;
Flagged transactions land in fraud-alerts within milliseconds of arrival. Your downstream system — whether that's a case management tool, a block/review decision engine, or a Slack webhook — consumes from that topic.
Flink vs. Streaming Database: What You're Actually Choosing
| Dimension | Apache Flink | RisingWave (Streaming DB) |
| Language for rules | Java / Scala / Table SQL | Standard SQL |
| State management | Manual (RocksDB config) | Automatic |
| Deployment | Flink cluster + JobManager | Single service or managed cloud |
| Rule iteration speed | Hours (build → deploy cycle) | Seconds (ALTER VIEW) |
| Queryable state | Limited | Full SQL queries on any view |
| Infrastructure cost | High (dedicated cluster) | Low (S3-backed, scales to zero) |
| Team skill requirement | Flink expertise | SQL knowledge |
| Open source license | Apache 2.0 | Apache 2.0 |
The comparison is not about capability. Flink can do everything RisingWave can and more. The question is whether you need that extra capability, and whether the cost — in engineering time, operational complexity, and hiring requirements — is worth paying.
For most fraud detection use cases, it is not.
Pattern Matching: Test Charge Followed by Large Charge
One classic fraud pattern is a small "probe" transaction to verify a stolen card is live, followed by a large fraudulent purchase. You can detect it with a self-join over a session window:
CREATE MATERIALIZED VIEW probe_then_spike AS
SELECT
a.card_id,
a.transaction_id AS probe_txn_id,
b.transaction_id AS spike_txn_id,
a.amount AS probe_amount,
b.amount AS spike_amount,
a.event_time AS probe_time,
b.event_time AS spike_time
FROM transactions a
JOIN transactions b
ON a.card_id = b.card_id
AND a.amount < 5.00 -- small probe charge
AND b.amount > 500.00 -- large follow-up
AND b.event_time > a.event_time
AND b.event_time < a.event_time + INTERVAL '30 minutes';
This runs continuously. When the pattern appears, the row appears in the view immediately.
Geographic Velocity (Card Present in Two Countries Simultaneously)
CREATE MATERIALIZED VIEW impossible_travel AS
SELECT
a.card_id,
a.country AS country_1,
b.country AS country_2,
a.event_time AS time_1,
b.event_time AS time_2,
ABS(EXTRACT(EPOCH FROM (b.event_time - a.event_time))) AS seconds_apart
FROM transactions a
JOIN transactions b
ON a.card_id = b.card_id
AND a.country <> b.country
AND b.event_time > a.event_time
AND b.event_time < a.event_time + INTERVAL '1 hour';
Two transactions on the same card from different countries within one hour is a strong fraud signal — or at least worthy of step-up authentication.
Operational Advantages You Don't Get With Flink
Rule iteration is instant. When your fraud team identifies a new pattern, you write a SQL view and it starts running immediately. No build pipeline, no deployment, no restart.
State is always queryable. With Flink, reading state requires custom tooling. With a streaming database, every materialized view is a live table you can query with any PostgreSQL client, connect to Grafana, or expose via your internal API.
No state backend tuning. RisingWave manages its own state, checkpointing to S3. You don't configure RocksDB memory limits or tune compaction.
Incident investigation is SQL. When a fraud analyst wants to know why a transaction was flagged, they run a SQL query. No Flink log diving required.
When Flink Is Actually the Right Choice
Flink makes sense when your fraud detection requires ML model inference inside the stream (not just SQL signals), extremely complex CEP patterns that exceed SQL expressiveness, or data volumes and latency requirements beyond what a single streaming database node handles.
At that scale and complexity, Flink's operational cost becomes justified. But most teams are not there yet — and they pay Flink's overhead long before they need its power.
FAQ
Can RisingWave handle the transaction volumes of a large fintech? RisingWave scales horizontally and stores state in S3, which means compute and storage scale independently. Production deployments handle millions of events per second. For very high-volume scenarios, you can partition by card prefix or user segment across multiple source topics.
How do I update a fraud rule without downtime?
In RisingWave, you can DROP MATERIALIZED VIEW and recreate it with new logic. For zero-downtime updates, create the new view alongside the old one, shift your downstream sink to the new view, then drop the old one. The whole process takes seconds.
Can I use this alongside an ML fraud model? Yes. A common pattern is to use RisingWave for rule-based pre-filtering (fast, cheap, catches obvious fraud), and route the remaining transactions to an ML scoring service. The ML scores can be written back into RisingWave as a source and joined with transaction signals.
Does RisingWave support exactly-once semantics? Yes. RisingWave provides exactly-once processing guarantees for sources and sinks that support it (including Kafka with transactional producers).
How does this compare to running fraud detection in a stream processor like Spark Streaming? Spark Structured Streaming has similar expressiveness to SQL but still requires cluster management, a Scala/Python codebase, and batch-oriented mental models for micro-batching. RisingWave is purpose-built for incremental streaming computation and typically achieves lower end-to-end latency (milliseconds vs. seconds).

