riskguard

module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT

README

riskguard

CI Go Reference Go Report Card

riskguard is a Go library for detecting fraud and abuse in payment transactions in real time: velocity limits, impossible-travel detection, amount thresholds, new-device checks, and blacklists, combined concurrently into a single risk score and decision (approve, review, or decline).

import "github.com/peymanahmadi/riskguard/pkg/riskguard"

What this repository actually is

This repo contains two things, and they are not peers.

  1. pkg/riskguard, the library. This is the deliverable. It has zero external dependencies, a small interface-driven API, and is what you'd go get and import into your own service.
  2. Everything else (cmd/server, internal/*), a reference integration. It exists to prove the library works under real conditions (HTTP, Postgres, Kafka, concurrent load) and to give you a working example to read. It's demo/test scaffolding, not something you're meant to depend on or deploy as-is. None of it is even importable outside this module, since it lives under internal/ and Go enforces that boundary.

If you only care about using riskguard in your own project, "Install" and "Quick start" below are all you need.

Install

go get github.com/peymanahmadi/riskguard/pkg/riskguard

Requires Go 1.22+. No other dependencies. pkg/riskguard imports only the standard library.

Quick start

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/peymanahmadi/riskguard/pkg/riskguard"
	"github.com/peymanahmadi/riskguard/pkg/riskguard/rules"
)

func main() {
	// Bring your own storage: anything satisfying CounterStore,
	// ProfileStore, or Blacklist works (Redis, DynamoDB, Postgres, an
	// in-memory fake for tests, see "Storage" below).
	counters := myCounterStore{}
	profiles := myProfileStore{}
	blacklist := myBlacklist{}

	engine := riskguard.NewEngine(
		riskguard.WithRules(
			rules.NewVelocityRule(counters, 5*time.Minute, 10),
			rules.NewAmountThresholdRule(50000, "USD"), // $500.00
			rules.NewGeoVelocityRule(profiles),
			rules.NewNewDeviceRule(profiles),
			rules.NewBlacklistRule(blacklist),
		),
		riskguard.WithScorer(riskguard.WeightedScorer{
			Weights: map[string]float64{"blacklist": 4, "geo_velocity": 3},
		}),
		riskguard.WithThresholds(riskguard.Thresholds{Review: 40, Decline: 75}),
		riskguard.WithTimeout(2*time.Second),
	)

	verdict, err := engine.Evaluate(context.Background(), riskguard.Transaction{
		ID: "tx_123", EntityID: "cust_1", AmountMinor: 75000, Currency: "USD",
		IP: "9.9.9.9", DeviceID: "phone-1", CreatedAt: time.Now(),
	})
	if err != nil {
		// Evaluate can return a usable Verdict and a non-nil error at the
		// same time under FailOpen, see "Failure handling" below.
	}

	fmt.Println(verdict.Decision)  // Approve, Review, or Decline
	fmt.Println(verdict.Score)     // 0-100
	fmt.Println(verdict.Reasons()) // human-readable reasons for any triggered rule
}

Why this shape

Payment-risk systems have a few recurring requirements, and they shaped the design of the library.

  • Low latency under load. Engine.Evaluate runs every rule concurrently instead of sequentially. Total latency tracks the slowest rule, not the sum of all of them. See docs/architecture.md for the mechanics and a throughput benchmark.
  • Explainability. Every Verdict carries the individual RuleResults that produced it, not just a final number. Verdict.Reasons() gives you a human-readable audit trail for support agents and compliance.
  • Resilience to partial failure. A single flaky rule, one whose store is slow or down, shouldn't be able to hang or crash evaluation for every other rule. The engine recovers from panics, enforces a per-call timeout, and lets you choose how to treat rule errors (see below).
  • Storage agnostic. Counters, profiles, history, and blacklists are each a small interface. Bring Redis, DynamoDB, Postgres, or a single-process in-memory map. The engine and rules never know or care.

Public API

The surface you build against lives in pkg/riskguard and pkg/riskguard/rules.

Type / function Purpose
riskguard.Transaction The input: a payment transaction to evaluate.
riskguard.Engine / NewEngine(opts...) Runs configured rules concurrently and produces a Verdict.
riskguard.Rule Interface for a single risk check. Implement this for custom rules.
riskguard.Scorer Interface for aggregating []RuleResult into one score. Built-ins: MaxScorer, WeightedScorer, SumCappedScorer.
riskguard.Verdict The output: Score, Decision, Results, Reasons().
riskguard.Thresholds Score cutoffs mapping onto Approve, Review, Decline.
riskguard.CounterStore Sliding-window event counts (velocity-style rules).
riskguard.ProfileStore Known devices, last location, running averages per entity.
riskguard.HistoryStore Recent transaction history per entity.
riskguard.Blacklist IP, device, or entity denylist checks.

Full generated docs: https://pkg.go.dev/github.com/peymanahmadi/riskguard/pkg/riskguard

Built-in rules (pkg/riskguard/rules)
Rule Detects
VelocityRule Too many transactions from one entity in a sliding time window (card testing, bot abuse).
AmountThresholdRule Transaction amount exceeding a configured threshold, scaled rather than a hard cliff.
GeoVelocityRule Impossible travel, where the location changes faster than is physically plausible (haversine distance divided by elapsed time).
NewDeviceRule Transaction from a device not previously seen for this entity.
BlacklistRule IP, device, or entity present in a blacklist.
Failure handling

Engine.Evaluate can return a non-nil error and a usable Verdict at the same time. That isn't a bug, it's how partial rule failure gets surfaced. Configure the behavior you want via WithFailurePolicy:

  • FailOpen (default): score using whichever rules succeeded. The error tells you coverage was degraded, but it doesn't block the decision.
  • FailClosed: any rule error forces Review, no matter what the successful rules concluded. Use this when missing signal is itself a risk.
Writing a custom rule
type Rule interface {
	Name() string
	Evaluate(ctx context.Context, tx Transaction) (RuleResult, error)
}

Implement those two methods and pass your rule to riskguard.WithRules(...) alongside (or instead of) the built-ins. No changes to the engine are needed. See pkg/riskguard/rules/*.go for five real examples, or riskguard.RuleFunc if you'd rather wrap a plain function than define a named type.

Reference integration (secondary, for proving it works)

The rest of the repo wires the library into an HTTP API, a Postgres-backed storage implementation, and a Kafka consumer/producer pipeline, so the library gets exercised under real concurrent load and not just unit tests. If you're evaluating this project rather than just using the library, this is the part worth reading to see the design decisions in context. Start with docs/architecture.md.

pkg/riskguard/            the library, start here
pkg/riskguard/rules/      built-in rules
cmd/server/                demo binary: wires the library to HTTP + Postgres + Kafka
internal/store/postgres/  example ProfileStore/HistoryStore/Blacklist/CounterStore backed by Postgres
internal/store/memory/    in-memory implementations (used by the demo and by rule unit tests)
internal/kafka/           example event-driven pipeline (transactions in, decisions out)
internal/api/             minimal HTTP handler for the demo server
test/integration/         tests against real Postgres/Kafka (docker compose)
docs/architecture.md       design rationale, sequence diagrams, why rules run concurrently

Running the demo locally:

go run ./cmd/server                                    # in-memory stores, no infra needed
# or, for the full stack:
make up      # docker compose up -d --build (postgres:16-alpine + kafka 8.2.2 + the server)
make demo    # fires a sample transaction at the HTTP API
make down

Verified end to end

This isn't just "it builds." It's been run against real infrastructure.

  • Unit tests: 80.7% coverage on pkg/riskguard and pkg/riskguard/rules (make cover), race-detector clean in CI.
  • Integration tests (make integration-test): pass against live Postgres and Kafka containers. Profile round trips, sliding-window counters, and a Kafka produce/consume round trip.
  • Concurrent-load benchmark: BenchmarkEngine_Evaluate comes in around 580µs/op, 16 allocs/op, evaluating 5 rules concurrently (make bench).
  • Manual load test: firing 15 rapid requests for the same entity correctly triggers VelocityRule starting at request 11 (limit 10), with the score climbing predictably as the window fills. That confirms the sliding-window counter and weighted scoring behave as designed under real repeated calls, not just mocked ones.

Known limitations

  • go test -race requires cgo, which isn't enabled by default on Windows. Use CI (Linux runners) for race detection, or install a GCC toolchain (MSYS2/mingw) or use WSL2 locally.
  • The in-memory CounterStore/ProfileStore (internal/store/memory) are single-process only. That's fine for the demo and for unit tests, but a multi-instance deployment needs a shared backend: the included Postgres implementation, or bring your own Redis/DynamoDB one.
  • AmountThresholdRule currently only judges amounts already in its configured currency. Multi-currency support means either composing one instance per currency or converting to a common currency upstream.

Versioning

Tagged releases follow semver. See CHANGELOG.md for what changed in each. The library lives at module path github.com/peymanahmadi/riskguard, and you import github.com/peymanahmadi/riskguard/pkg/riskguard and github.com/peymanahmadi/riskguard/pkg/riskguard/rules.

License

MIT. See LICENSE.

Directories

Path Synopsis
cmd
server command
Command server is a small demo application showing riskguard wired up end-to-end: a Postgres- or memory-backed engine exposed over HTTP, plus an optional Kafka consumer/producer pipeline for asynchronous evaluation.
Command server is a small demo application showing riskguard wired up end-to-end: a Postgres- or memory-backed engine exposed over HTTP, plus an optional Kafka consumer/producer pipeline for asynchronous evaluation.
internal
api
Package api exposes the riskguard engine over a small JSON HTTP API for the demo service in cmd/server.
Package api exposes the riskguard engine over a small JSON HTTP API for the demo service in cmd/server.
kafka
Package kafka wires the riskguard engine into an event-driven pipeline: transactions arrive on an input topic, get evaluated, and the resulting decision is published to an output topic for downstream consumers (ledger, notification service, case management, etc).
Package kafka wires the riskguard engine into an event-driven pipeline: transactions arrive on an input topic, get evaluated, and the resulting decision is published to an output topic for downstream consumers (ledger, notification service, case management, etc).
store/memory
Package memory provides in-memory implementations of riskguard's storage interfaces.
Package memory provides in-memory implementations of riskguard's storage interfaces.
store/postgres
Package postgres provides Postgres-backed implementations of riskguard's storage interfaces (ProfileStore, HistoryStore, Blacklist, CounterStore), suitable for multi-instance production deployments where the in-memory store (internal/store/memory) would not share state across replicas.
Package postgres provides Postgres-backed implementations of riskguard's storage interfaces (ProfileStore, HistoryStore, Blacklist, CounterStore), suitable for multi-instance production deployments where the in-memory store (internal/store/memory) would not share state across replicas.
pkg
riskguard
Package riskguard provides a pluggable, concurrency-safe engine for evaluating payment transactions against a set of fraud/abuse detection rules and producing a risk decision (approve / review / decline).
Package riskguard provides a pluggable, concurrency-safe engine for evaluating payment transactions against a set of fraud/abuse detection rules and producing a risk decision (approve / review / decline).
riskguard/rules
Package rules contains concrete, production-oriented riskguard.Rule implementations.
Package rules contains concrete, production-oriented riskguard.Rule implementations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL