Fraud alert system
Real-time fraud detection for payments. Every transaction is scored against configurable rules and gets an approve, soft-block, or hard-block decision in the request path, backed by an event-driven audit trail that cannot be quietly changed.
Overview
When a payment is authorized, the decision has to block: the money does not move until the system says it is safe. This platform sits on that path. A client calls the gateway, the payment service asks the fraud service to evaluate the transaction, and a decision comes back in the request, not minutes later in a report.
Three decisions come out of it. Approve lets the payment through. Soft-block stops a suspicious payment but lets the customer acknowledge the risk and proceed. Hard-block refuses outright, with no override. Behind every decision is an immutable record of why.
On a payment path, the safe default is no. The system fails closed: if it cannot verify a transaction, it does not approve it.
How a decision is made
A small rule engine scores each transaction. Most rules add to a risk score; cross a threshold and the payment is soft-blocked. One rule is absolute: an extreme value hard-blocks on its own, whatever the rest of the score says. Rules are configurable at runtime, and adding a new one is a new strategy class, not a change to the engine.
Each rule is a pure function with its data pre-fetched, and each runs in isolation: one misconfigured rule cannot take the fraud gate offline, and a bad parameter falls back to that rule's default rather than silently switching detection off.
The engineering
The system is four .NET services behind a YARP gateway that handles routing, API keys, rate limiting, and a correlation id stamped on every request. The same fraud rule engine is reachable two ways: the synchronous authorize call, and a Kafka consumer that reads a transaction stream. Both run identical evaluation logic, so the two entry points can never drift apart.
The hard part of an event-driven system is the dual write: updating the database and publishing an event have to succeed or fail together. This uses the transactional outbox pattern. The service writes its state change and the outgoing event in one database transaction, and a separate relay publishes the event to Kafka afterwards. Nothing is lost if the process dies between the two.
The stream side is built to survive bad input. Delivery is at-least-once and every handler is idempotent on the transaction id, so a redelivered event returns the prior result instead of double-counting. Malformed messages are quarantined to a dead-letter topic, and transient failures retry with exponential backoff before dead-lettering, so one poison message never blocks the partition.
Every evaluation lands in an append-only audit trail. A dedicated worker consumes the events and writes them to their own schema, so the record of what happened is separate from the services that make decisions and is never edited in place. Structured logs carry the same correlation id from the gateway through to the audit row, so a single transaction can be traced end to end.
Built for production
Data ownership is kept clean: payment, fraud, and audit each own their own schema in one PostgreSQL instance, with Entity Framework migrations applied on startup. The services are stateless and scale by replica. Kubernetes manifests define the deployments, liveness and readiness probes, a config and secret split, and graceful shutdown on SIGTERM, and the whole stack runs the same images in Docker for local work.
The rule engine and the flows are covered by unit, integration, and end-to-end tests, including a test that pins the stream event to the same shape as the authorize request so the two paths stay in lockstep. The design decisions behind it, the outbox, fail-closed, the strategy pattern for rules, schema separation, and the resilience policy, are each written down as an architecture decision record.