Rule Cascade
Playbooks

Enforce in a backend API

Load the bundle at start-up, evaluate every state-changing operation on the server channel, answer 422 on deny, persist, then run commands once. Java, Node.js, Go and Python.

The backend holds the decision. Whatever the browser reported, every state-changing operation is evaluated again on the server channel, with facts only the server has.

Before you start

Steps

Load at start-up; refuse to start on a load error

@SuppressWarnings("unchecked")
RuleSet rules = RuleSet.fromBundle((Map<String, Object>) Json.parse(Files.readString(bundlePath)));
// LoadException propagates out of the @Bean method and stops the application.

Verify the custom operators

A manifest lists the operators its rules need. Refuse to start when one is not registered.

RuntimeThe operators the server manifest needs
Javarules.requiredOperators(Channel.SERVER)
Node.jsmissingOperators(rules.manifest('server'), operators) returns those not in operators
Gomanifest.Get("operators") on the result of rules.Manifest("server")
Pythonrules.manifest("server").get("operators", [])

Evaluate every state-changing operation from the server's own facts

MemberSource
entity, operationThe x-rule-cascade tag of the API operation, never the caller
dataThe body after shape validation (400 before any rule runs), plus the fields the server assigns
originalThe stored record, for every operation except create
actorid and roles from authentication, never from the payload or a caller-set header
ctx.nowThe server clock
resolutionsThe resolutions member of the body

Deny with 422, or persist with the computed values and the commands

From TransferController in the Spring Boot example, which CI builds and tests:

EvaluationRequest.Builder builder = EvaluationRequest.builder(ENTITY, operation)
        .data(data)
        .original(original)
        .actor(actorId, roles);

EvaluationResult result = rules.evaluate(request);
if (!result.allowed()) {
    throw new RuleViolationException(result);   // ApiExceptionHandler: 422 problem details
}
EvaluationResult result = check("create", transfer, null, actorId, roles, body);
applyComputedValues(transfer, result);
store.put((String) transfer.get("id"), transfer);
run(result);                                    // each command once per idempotencyKey()
return ResponseEntity.status(HttpStatus.CREATED).body(envelope(transfer, result));

store, log, caller, operators and newID stand for your persistence with its outbox, your logger, the authenticated principal, your custom operators and your id generator. The four listings are those of enforcement guide step 4.

Run each command once, after the commit

A relay publishes the outbox after the transaction commits and de-duplicates on idempotencyKey. The engine returns commands only for an allowed server evaluation, so a denied operation never produces one. See post-save commands.

Test the server-only path

Write a test that sends a request a server-only rule denies (in the payments example, a transfer to a blocked country) and asserts 422 and that nothing was stored. The Spring Boot example's TransferApiTest.aBlockedCountryIsRefusedWithProblemDetails asserts the 422 and the finding through MockMvc; add the "nothing stored" assertion against your own store.

Done when no state-changing endpoint can persist anything before an allowed server evaluation; a request denied by a server-only rule returns 422 and changes nothing; every decision is logged with ruleset id, version and checksum; and a load error or a missing operator stops start-up.

Audit the result against the backend checklist.

Source: site/content/docs/playbooks/backend-api.mdx

On this page