Rule Cascade on the JVM
Dependency (Maven): com.rulescascade:rules-cascade-core (Java 17+, no other runtime dependency;
package com.rulescascade). Gradle: implementation("com.rulescascade:rules-cascade-core:<version>").
Bundles come from rcas compile --all into build/rules/. Never evaluate the YAML at run time and never
copy a rule into code.
Load once, evaluate per request
import com.rulescascade.*;
// One bean / singleton, created at start-up. An exception here must stop the application.
RuleSet rules = RuleSet.fromBundle((Map<String, Object>) Json.parse(Files.readString(Path.of("build/rules/<ruleset-id>.bundle.json"))));
List<String> missing = rules.missingOperators(); // custom operators the rules need
if (!missing.isEmpty()) throw new IllegalStateException("unregistered operators: " + missing);
When the service parses JSON with Jackson, enable DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
so amounts reach the engine exactly.
Prefer rules.enforce(request) (from 1.0.0-alpha.7): it returns the allowed result or throws
com.rulescascade.RuleViolationException, whose problem() is the 422 answer, the same in every
Rule Cascade runtime. One handler answers it for every endpoint:
@RestControllerAdvice
class RuleErrors {
@ExceptionHandler(RuleViolationException.class)
ResponseEntity<Map<String, Object>> denied(RuleViolationException e) {
return ResponseEntity.status(e.status()).contentType(MediaType.APPLICATION_PROBLEM_JSON).body(e.problem());
}
}
For an annotation on controller methods, copy @EnforceRules and RuleEnforcementAspect from the
Spring Boot example (https://rulescascade.com/reference/examples/backend-spring-boot/); it needs
spring-boot-starter-aspectj (spring-boot-starter-aop before Spring Boot 4). Every state-changing
endpoint follows four steps:
- Evaluate on the server:
rules.evaluate(EvaluationRequest.builder(entity, operation).data(data).original(original).actor(...).build(), Channel.SERVER). The actor comes from the security context, never from a header the caller controls. - Refuse on deny: when
!result.allowed(), throw aRuleViolationException(result)and map it once, in a@RestControllerAdvice/ exception mapper, to 422ProblemDetailwithtype: urn:rule-cascade:rule-violationand the result (result.toMap()) as a property. - Persist, after applying
computeeffects (effect.type()isvalue): the server's value is stored. - Run
result.commands()after the commit (@TransactionalEventListener(phase = AFTER_COMMIT)or an outbox), once peridempotencyKey.
Placement: Spring a @Configuration with a RuleSet @Bean and a service per entity that builds the
request; Quarkus/Micronaut a @Singleton producer. Kotlin and Scala call the same API.
RuleSetHolder reloads bundles on an interval or a cron schedule and keeps the last good rules.
Replacing existing validation
rcas analyze --module <module> lists Bean Validation annotations, Spring validators and
@PreAuthorize, Drools .drl rules, hand-written checks, named limits (MAX_QUANTITY = 10),
comparisons with numbers and switch branches on a status. A candidate with drift means the same
name has another value in the front end: one ruleset must decide it for both. Keep @NotNull/@Size type
checks on the DTO if you like; move business decisions to rules. Add a parity test (JUnit
@ParameterizedTest over the same inputs for the old check and the engine), then delete the old check.
Tests
Assert decision and finding codes, not message texts. Golden tests in the ruleset (rcas check)
test the rules; CI runs rcas check and rcas compile --all before mvn verify / gradle test.
Reference: https://rulescascade.com/usage/java/ and https://rulescascade.com/playbooks/backend-api/