API endpoint
A payments API that loads the ruleset at start-up and, on every operation, evaluates, refuses with 422, persists, and runs the commands once.
Code: examples/backend-spring-boot
(README). CI builds it and runs its MockMvc tests on
every push. The Node.js, Go and Python tabs are the listings of
enforcement guide step 4, for the same
operation. Runnable Node.js and Go services with the same four steps (evaluate, 422 on deny, persist,
run commands once per idempotency key) are in
examples/backend-node and
examples/backend-go, each tested by CI.
Start-up: load, and refuse to start on a gap
From RuleCascadeConfiguration.java:
RuleSet loaded = bundleLocation.isBlank() ? compile(location, rulesetId) : readBundle(bundleLocation, rulesetId);
// The manifest lists the custom operators its rules need. Refuse to start when one is missing:
// at evaluation time a rule that calls an unregistered operator can only fail closed.
Map<String, CustomOperator> operators = CustomOperators.all();
List<String> missing = new ArrayList<>(loaded.requiredOperators(Channel.SERVER));
missing.removeAll(operators.keySet());
if (!missing.isEmpty()) {
throw new IllegalStateException("ruleset " + loaded.id() + " needs custom operators that are not registered: " + missing);
}
return loaded.withOperators(operators);rule-cascade.bundle in application.yml switches from compiling the YAML contracts to reading a
bundle compiled in CI.
Every operation: evaluate, refuse, persist, run
From TransferController.java:
@PostMapping
public ResponseEntity<Map<String, Object>> create(
@RequestBody Map<String, Object> body,
@RequestHeader(value = "X-Actor-Id", defaultValue = "anonymous") String actorId,
@RequestHeader(value = "X-Actor-Roles", defaultValue = "") List<String> roles) {
Map<String, Object> transfer = new LinkedHashMap<>(entity(body));
transfer.put("id", UUID.randomUUID().toString());
transfer.put("status", "draft");
transfer.put("createdBy", actorId);
EvaluationResult result = check("create", transfer, null, actorId, roles, body);
applyComputedValues(transfer, result);
store.put((String) transfer.get("id"), transfer);
run(result);
return ResponseEntity.status(HttpStatus.CREATED).body(envelope(transfer, result));
}check builds the EvaluationRequest with the resolutions of the body, evaluates, and throws
RuleViolationException when result.allowed() is false; ApiExceptionHandler turns it into a
422 ProblemDetail of type urn:rule-cascade:rule-violation with result.toMap() as its
evaluation property.
The example reads the actor from two headers to stay short. A real service takes it from its security context.
Try it
mvn -f packages/java/pom.xml install
cd examples/backend-spring-boot && mvn spring-boot:run# 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"}}}'The answer is 422 with the finding ORG-TRF-001 ("Transfers to KP are not permitted.") in
evaluation.findings; TransferApiTest.aBlockedCountryIsRefusedWithProblemDetails asserts exactly
that. The README lists the other requests to try, including a risk acceptance by a risk-officer.
Step by step: Enforce in a backend API.