Spring Boot example
A small payments API that enforces examples/contracts/payments-transfer.ruleset.yaml with the Java runtime.
A small payments API that enforces examples/contracts/payments-transfer.ruleset.yaml with the Java runtime.
| File | What it shows |
|---|---|
RuleCascadeConfiguration | Load the ruleset once at startup, either compiled from the YAML contracts or read from a precompiled bundle; a bad ruleset or a missing custom operator stops the application |
CustomOperators | Custom operators (x-*) the service supplies to its rulesets |
TransferController | The four steps on every operation: evaluate, refuse on deny, persist, run commands |
RuleCascadeController | Serve the client-safe manifest with an ETag, and a dry-run evaluation endpoint |
ApiExceptionHandler | Return a denied evaluation as RFC 9457 problem details (422) |
TransferApiTest | The behaviour above, end to end through MockMvc, with the ruleset compiled from source |
BundleStartupTest | The same service started from a bundle |
CustomOperatorTest | A ruleset that needs a custom operator: with it registered, and failing closed without it |
Run it
Not yet run where this repository was assembled (Maven Central was unreachable there); CI builds and tests it.
mvn -f ../../packages/java/pom.xml install # puts rule-cascade-core in your local Maven repository
mvn spring-boot:run# Allowed, with a non-blocking warning about the missing memo
curl -s localhost:8080/transfers -H 'Content-Type: application/json' -d '{
"transfer": {"type": "domestic", "amount": 120.50, "currency": "USD",
"beneficiary": {"name": "Jo Lee", "country": "US"}}}'
# Denied by a server-only rule the browser never sees
curl -s localhost:8080/transfers -H 'Content-Type: application/json' -d '{
"transfer": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
"beneficiary": {"name": "X", "country": "KP", "swiftCode": "ABCDKPPY"}}}'
# Over the limit: only a risk officer can accept the risk, with a justification
curl -s localhost:8080/transfers -H 'Content-Type: application/json' \
-H 'X-Actor-Id: u-9' -H 'X-Actor-Roles: risk-officer' -d '{
"transfer": {"type": "domestic", "amount": 30000, "currency": "USD", "memo": "house",
"beneficiary": {"name": "Sam", "country": "US"}},
"resolutions": [
{"rule": "transfer.large.review-warning", "type": "acknowledge"},
{"rule": "org.transfer.amount-limit", "type": "accept-risk", "justification": "verified source of funds"}]}'
# What the React app fetches
curl -si localhost:8080/rulesets/acme.payments.transfer/manifest | head -20Source or bundle
By default the service compiles contracts/*.ruleset.yaml from the classpath at startup
(RuleSet.load). Set rule-cascade.bundle to start from a compiled bundle instead
(RuleSet.fromBundle); the service then needs no YAML and repeats no load-time check:
# from the repository root: compile the contract into a bundle
python tools/rulecheck.py compile examples/contracts/payments-transfer.ruleset.yaml \
-o /tmp/acme.payments.transfer.bundle.json
# from this directory: start from the bundle
mvn spring-boot:run \
-Dspring-boot.run.arguments=--rule-cascade.bundle=file:/tmp/acme.payments.transfer.bundle.jsonrule-cascade compile (the Go command) writes the same bundle; any compiler does.
A bundle contains the server manifest, so treat it like any other internal artefact: build it in CI,
store it immutably, and never serve it to a browser. The tests read the bundles published in
conformance/bundles, which are the compiled form of the example contracts.
Custom operators
A ruleset may use operators the host application supplies. They are registered once, in
CustomOperators, and attached with RuleSet.withOperators. At startup the configuration asks the
ruleset for its missingOperators() and refuses to start when there is one.
The payments contract declares no custom operator; CustomOperatorTest evaluates the onboarding
catalog, whose loyalty-number rule calls x-luhn.
The actor
The actor comes from two request headers to keep the example short. A real service takes the actor from its security context and never trusts a role a caller claims for itself.
Node.js example (node:http)
The payments API of backend-spring-boot, on plain node:http with the TypeScript runtime and no other dependency. It enforces payments-transfer.ruleset.yaml from its compiled bundle.
Batch recipe
Evaluate a file of thousands of operations against one ruleset, with any runtime, and write one decision per line.