LibEntity Decision
Overview
LibEntity Decision is a decision engine that can be used to make decisions based on a meta-model for business rules. It consolidates LibEntity by bringing structure to this very sensitive aspect of business entities.
Given the action and transition structure, one can use ordinary java code to implement the business rules. But over time, rules tend to fall through the cracks and become obscure, hard to maintain and understand.
LibEntity Decision addresses this issue by providing a decision engine somewhat inspired by decisions4s.
Example
Setup
Make sure to include the annotation processor in your build tool.
Grandle:
annotationProcessor 'com.libentity:lib-entity-decision:<latest-version>
Define your input type
Similar to decisions4s, you can define your input type using a class.
@AllArgsConstructor
@DecisionInput
public class InvoiceInput {
Rule<String> requesterId;
Rule<LocalDate> isDateSet;
Rule<BigDecimal> amount;
Rule<String> isApproved;
}
Important
At the moment only classes with public/default attributes are supported. Soon records and access via getters will be supported.
The @DecisionInput
annotation will generate a value class and a InputProvider
interface implementation. More on that later.
Create your rules
The Rule
type is a type-safe way to define rules for each attribute of the input type. Here is how to define a series of MatchingRules
that will be used by our decision table.
var blockedRequesters = Set.of(
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
"98c8627c-2203-4f0b-8d0a-adea7150f6c6");
var rules = List.of(
MatchingRule.of(
new InvoiceInput(
// in matches if the attribute is in the set
in(blockedRequesters),
// it is true if the attribute is present
isPresent(),
// gt fo greater than
gt(BigDecimal.valueOf(0.0)),
any()),
// This is the expected output that can be any object
Boolean.FALSE,
inputProvider),
MatchingRule.of(
new InvoiceInput(
// catch all
any(),
// catch all
any(),
// grater than 1.0
gt(BigDecimal.valueOf(1.0)),
// catch all
any()),
Boolean.TRUE,
inputProvider));
Evaluate
With the rules at hand, we can create a decision table.
var decisionTable = DecisionTable.of(rules);
And finally, we can use the decision table to make decisions.
var decision = decisionTable.evaluateFirst(
// InvoiceInputValue is the generated by the framework
new InvoiceInputValue(
"a0834601-041b-4837-a2fd-293802cc255f",
LocalDate.now(),
BigDecimal.valueOf(1.0),
"true"));
Diagnose
The decision table result can be inspected by calling decision.diagnose()
. The output should be similar to:
Hit Policy: First
Result: Optional[true]
Input:
requesterId: user1
isDateSet: 2025-05-29
amount: 100
isApproved: "true"
Rule 0 [f]:
requesterId [f]: in( 2a9ad172-9f2f-4dbc-9093-6dbb20b5b07e, 98c8627c-2203-4f0b-8d0a-adea7150f6c6, 95c50ae5-4b4a-44f7-b8ca-fe00b7de8524 )
isDateSet [t]: isPresent
amount [t]: > 0.0
isApproved [t]: -
== x
Rule 1 [t]:
requesterId [t]: -
isDateSet [t]: -
amount [t]: > 1.0
isApproved [t]: -
== true
The way to read is as follows:
Hit Policy
:First
means that the first matching rule output will be used as the final result of the evaluation.Result
:Optional[true]
means that the first matching rule output wastrue
.Input
: Outputs the values given to the decision table.Rule x
[f/t]: For eachMatchingRule
outputs each attribute match result.f
means false andt
means true.
Hit Policies
LibEntity Decision provides a few hit policies:
First
: Returns the first matching rule output.Unique
: Returns the first matching rule as long as there are no other rule also matching the input. Useful for cases where only one rule should match and more than one trigger may indication some unexpected behavior.Collect
: Returns the output of all rules.Sum
: Allows the call site to provide aBinaryOperator
on the result of distinct match policy outcomes.
Matchers
The rules can be formed by arbitrary code, or preferredly by using matchers. Here are the available matchers:
Rule.any()
: Matches any value. Always true.Rule.in(Set<T> target)
: Matches if the value is in the set.Rule.gt(T target)
: Matches if the value is greater than the given value.Rule.ge(T target)
: Matches if the value is greater than or equal to the given value.Rule.lt(T target)
: Matches if the value is less than the given value.Rule.le(T target)
: Matches if the value is less than or equal to the given value.Rule.is(T target)
: Matches if the value is equal to the given value.Rule.isPresent()
: Matches if the value is not null. If it's an Optional, it matches if it is present.
Boolean logic
The especial Rule.test
takes a predicate and returns a Rule<T>
that matches if the predicate returns true
. The produced Rule
for each matcher can be used to be negated with a not()
call. For example Rule.is("Samba").not().evaluate("Rock")
will evaluate to true
.
Similarly, Rule<T>.and(other)
and Rule<T>.or(other)
can be used to combine rules into more complex structures.
Why Not Just Write Plain Java?
For small or isolated decisions, using plain Java logic might seem simpler:
if (blockedRequesters.contains(requesterId)
&& isDateSet != null
&& amount.compareTo(BigDecimal.ZERO) > 0) {
return false;
}
if (amount.compareTo(BigDecimal.ONE) > 0) {
return true;
}
return false;
While this is readable at first glance, it becomes problematic as business rules evolve:
❌ Scattered logic: Rules get embedded across services, controllers, and utilities.
❌ Poor traceability: It’s hard to answer “Why did this payment transition fail?” or “What rule applied?”
❌ Limited diagnostics: No built-in insight into which rule matched or failed.
❌ Hard to test in isolation: Rules are often coupled to state and environment.
❌ High change risk: Modifying logic can introduce subtle regressions.
Advantages of LibEntity Decision LibEntity Decision was designed to mitigate these issues:
✅ Centralized rules: Business rules are defined in one place using structured, testable constructs.
✅ Declarative API: Rules are expressed as data, not control flow, making them easier to read and change.
✅ Traceability: Rule matches are fully diagnosable via .diagnose()
, allowing introspection into decision outcomes.
✅ Policied evaluation: Choose between First
, Unique
, Collect
or Sum
evaluation strategies for different business needs.
✅ Auto-generated support: @DecisionInput
eliminates boilerplate and enforces type safety.
✅ Composable matchers: Clean, reusable, and composable rule conditions like isPresent()
, in(...)
, gt(...)
, etc.
Feature | Manual Java Logic | LibEntity Decision |
---|---|---|
Code structure | Imperative, scattered | Declarative, centralized |
Diagnosability | Manual logging | Built-in .diagnose() |
Rule composition | Custom logic per case | Matchers & rules |
Maintainability | Risky with complexity | Scales with complexity |
Testability | Often tightly coupled | Fully testable in isolation |
Evolution of logic | Requires refactoring | Rules can be reordered safely |
Future explorations
At the moment evaluation happens eagerly and Policies are used as a way to extract the results. Future explorations may include a lazy evaluation approach, moving the evaluation to each policy.
The remaining Policies available for decisions4s will be implemented.
A Lib-Entity Action Handler backed by a decision will be made available for a tight integration.