Real-Time Fraud Detection: Why Flink Is Overkill for Most Teams

Real-Time Fraud Detection: Why Flink Is Overkill for Most Teams

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.


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:

  1. Velocity checks — how many transactions has this card/user done in the last N minutes?
  2. Threshold alerts — is this amount unusually large compared to this user's history?
  3. Pattern matching — is this sequence of transactions a known bad pattern (e.g., small test charge followed by large charge)?
  4. 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.


DimensionApache FlinkRisingWave (Streaming DB)
Language for rulesJava / Scala / Table SQLStandard SQL
State managementManual (RocksDB config)Automatic
DeploymentFlink cluster + JobManagerSingle service or managed cloud
Rule iteration speedHours (build → deploy cycle)Seconds (ALTER VIEW)
Queryable stateLimitedFull SQL queries on any view
Infrastructure costHigh (dedicated cluster)Low (S3-backed, scales to zero)
Team skill requirementFlink expertiseSQL knowledge
Open source licenseApache 2.0Apache 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.


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.


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).

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.