Engineering Notes

Building Production Intelligent Risk Control: Streaming, Lakehouse, Rules, and Models

A production architecture for low-latency, replayable, auditable, and degradable risk decisions using Kafka, Spark Structured Streaming, CDC, Hadoop/Iceberg, rules, and models.

The hardest part of risk control is not expressing a rule such as “five failed logins from one device in ten minutes.” It is making a stable, explainable, and traceable decision when events arrive out of order, messages repeat, services fail, strategies change, and traffic peaks at the same time.

A mature system must answer four questions:

Production risk control is therefore neither one Spark job nor one rule repository. It is a decision system composed of synchronous serving, asynchronous stream processing, an offline lakehouse, strategy operations, and a feedback loop.

Architecture: two paths, one fact history, full replay

Production real-time and offline risk architecture

The key separation is between synchronous interception and real-time computation:

  1. Synchronous online path: a Risk API reads online features, evaluates rules and models, and returns allow, deny, or review within a strict latency budget.
  2. Asynchronous streaming path: business events enter Kafka; Spark Structured Streaming performs event-time deduplication, sliding windows, stateful aggregation, and feature updates.
  3. Offline batch path: immutable facts land in a lakehouse on HDFS or object storage; Spark Batch handles replay, reconciliation, backfill, rule simulation, and model datasets.
  4. Unified strategy and audit: features, rules, models, and decision records are versioned so an online outcome can be reproduced offline.

This is safer than blocking every business request on Spark. Structured Streaming normally executes in micro-batches and is well suited to second-level state updates. A transaction requiring consistently low tens-of-milliseconds latency needs an independently deployed serving path so scheduler delays, backpressure, or checkpoint pauses cannot enter the critical request path.

Establish an immutable risk fact stream

Kafka should be treated as a durable risk event log, not temporary plumbing. Payments, logins, devices, account changes, list updates, and investigation outcomes become governed events.

Every event should contain at least:

event_id          globally unique identity for deduplication and tracing
event_type        business meaning and semantic version
event_time        when the business event actually occurred
ingest_time       when the platform received it
entity_keys       user_id / account_id / device_id / ip
payload           facts rather than transient computed judgments
trace_id          correlation across request, stream, and decision
schema_version    compatible evolution
source            producing system and environment

Design topics around business facts and retention requirements, not one topic per rule. A partition key should match the stateful computation: account_id preserves account order, while network detection may require device_id. Derive separately keyed streams rather than expecting one key to serve every use case.

Use idempotent producers, strong acknowledgements, and governed Avro, Protobuf, or JSON Schema. Consumers still require idempotency because end-to-end exactly-once behavior depends on the external sink. A practical system target is at-least-once transport + business-key deduplication + idempotent writes + reconciliation and replay.

Do not poll operational databases aggressively. Capture account state, lists, and merchant reference data through log-based CDC. When a database update and a domain event must agree, use a Transactional Outbox to avoid the dual-write gap between committing data and publishing a message.

Define sliding windows in event time

Risk windows must use event_time, not the time Spark happens to process a record. Mobile disconnection, network delay, and Kafka backlog cause disorder; processing-time logic can produce different outcomes for the same facts after a restart.

Common windows include:

1-minute window sliding every 10 seconds: transaction burst
10-minute window sliding every minute: failures and amount velocity
24-hour window sliding every 15 minutes: accounts per device
7-day state: new beneficiary, familiar geography, behavioral baseline

Window length expresses observation range; slide interval expresses update frequency. Smaller slides increase state and compute cost. Derive them from the maximum acceptable detection delay rather than setting every feature to one second.

A watermark says how long the system is willing to wait for late data and when it may clear state. It does not guarantee that all records arrive within that delay. Choose it from measured lateness—perhaps covering 99.9% of events—and route records beyond the threshold to a late-data path for backfill, audit, and monitoring rather than silently discarding them.

events = (
    spark.readStream.format("kafka")
    .option("subscribe", "risk.payment.v1")
    .load()
    .transform(parse_and_validate)
    .withWatermark("event_time", "10 minutes")
    .dropDuplicatesWithinWatermark(["event_id"])
)

velocity = events.groupBy(
    window("event_time", "10 minutes", "1 minute"),
    "account_id",
).agg(
    count("*").alias("tx_count_10m"),
    sum("amount").alias("tx_amount_10m"),
)

This illustrates semantics, not a copy-ready production job. Real pipelines also need schema validation, quarantine, state bounds, isolated checkpoint locations, rate control, monitoring, and idempotent sinks.

Streaming features must leave Spark memory

Window results, recent entity sets, velocity metrics, and risk counters belong in a low-latency online store read by the synchronous decision service. Redis, Cassandra, HBase, or an existing highly available KV store can work, but first define a feature contract:

feature_name + entity_key + value
event_time + computed_at
definition_version + producer_job_version
ttl + freshness_sla

Writes must be idempotent. With foreachBatch, use a query/batch identity or business window key, plus an upsert, transactional table, or commit log. Spark checkpoints can restore source offsets and computation state; they do not automatically make an arbitrary external database exactly-once.

Define stale-feature behavior explicitly. When the store is unavailable or freshness exceeds its SLA, the engine must distinguish a true zero from missing data and choose a conservative rule set, fallback model, manual review, or constrained approval according to risk.

Rules and models are complementary

A mature decision combines several mechanisms:

Hard rules: regulation, deny lists, unambiguous prohibitions
Velocity rules: counts, amounts, linked entities, time windows
Model scores: fraud, anomaly, account-takeover probability
Policy orchestration: matches + scores + business cost → action

Rules fit deterministic, explainable, rapidly changing constraints. Models fit multivariate patterns that are difficult to hand-code. The final action should incorporate exposure, customer value, false-positive cost, and available review capacity—not only one score.

A rule platform needs version, state, priority, audience, effective interval, author, approver, and rationale. The release path should be draft → tests → historical replay → shadow → canary → full rollout. Record the rule version and input snapshot for every match, but avoid synchronous heavy audit writes in the high-QPS path; publish a reliable decision event and persist it asynchronously.

Keep the rule DSL constrained and analyzable. Arbitrary scripts should not access the network or query production databases per transaction. Reference data should arrive through governed interfaces or preloaded snapshots.

Modernize the Hadoop layer into a governed lakehouse

HDFS remains appropriate for distributed storage in on-premises Hadoop deployments; cloud systems commonly use object storage. In either case, bare Parquet directories are a weak sole data-management layer. An open table format such as Iceberg adds atomic commits, schema and partition evolution, snapshots, and time travel—strong foundations for replay and audit.

Use at least three layers:

Spark Batch performs reconciliation, historical windows, feature backfills, fraud labels, and strategy evaluation. Batch and streaming should share feature definitions or be generated from the same declarative logic. Training datasets require point-in-time joins that use only information available at decision time, preventing future leakage.

Streaming into Iceberg creates frequent snapshots and small files. Run separate maintenance for compaction, manifest rewriting, snapshot expiration, and metadata monitoring. Retention is also a product decision: raw facts, features, decisions, and sensitive identifiers need distinct retention and deletion policies.

Every decision must be replayable

Persist a Decision Record containing:

decision_id / request_id / trace_id
event_id and raw-fact location
feature values, event times, and definition versions
matched rules and versions
model name, version, score, and threshold
final action, reason code, and human override
decision time, latency, and degradation state

Audit supports compliance and operations. It lets the team explain why a customer passed yesterday and failed today, whether a rule caused false positives, and whether model deterioration came from drift, stale features, or a business distribution shift.

Use stable reason codes. Operational explanations and customer-facing explanations should be designed separately so staff can act on them without revealing exploitable strategy details.

Availability requires explicit degradation semantics

Production design defines behavior under failure:

FailureRecommended behavior
Kafka backlogContinue with timestamped recent features; monitor freshness and activate conservative policy
Online store unavailableUse local cache and core rules; send high-risk requests to review or deny
Model timeoutCircuit-break quickly; fall back to rules and the last stable model
Bad rule releaseRoll back immediately; require dual approval and shadowing for high-impact rules
Streaming job failureRecover from durable checkpoints; replay from Kafka and the archived facts
Data-quality incidentQuarantine bad events, freeze affected features, alert ownership, and stop propagation

Deploy the decision service across failure domains and assign an end-to-end latency budget plus dependency timeouts. Kafka, checkpoints, online stores, and lakehouse each need RPO/RTO. Disaster recovery is not “the process restarted”; periodically prove recovery from offsets, checkpoints, and immutable history.

Observe systems, data, and decisions

CPU and latency are insufficient. Operate three metric families:

Alerts should describe impact. Rising lag matters because particular features crossed their SLA and a number of decisions entered degradation—not simply because a threshold turned red.

A practical delivery sequence

1. Facts and the minimum decision loop

2. Real-time state

3. Rule operations and replay

4. Models and continuous optimization

The standard for production maturity

A production risk system should demonstrate that:

  1. synchronous latency is bounded and streaming cannot stall the transaction path;
  2. Kafka holds replayable facts, with explicit duplicate, disorder, and lateness semantics;
  3. online and offline features share definitions, while rules and models are versioned;
  4. every decision is explainable, auditable, and reproducible;
  5. each dependency has a deliberate degradation policy;
  6. success is measured through loss prevented, false-positive cost, and customer experience.

Risk control is not the pursuit of maximum rejection. It is the discipline of making cost-aware, evidence-based, accountable decisions from changing information under time pressure. Kafka, Spark, Hadoop, and a rule engine are infrastructure. The real system is the learning loop that joins live facts, historical evidence, and business judgment.

Further reading

Back to Engineering Notes