Persistence
Decisions4s supports loading decision tables from external sources using expression languages. This allows storing decision logic in databases, configuration files, or receiving them from external services, while maintaining type safety at the Scala level.
This set of features is experimental. The API or behavior may change in future versions. We welcome all feedback - please open an issue on GitHub if you encounter any problems or have suggestions.
Overview
The persistence feature consists of:
- DecisionTableDTO: A language-agnostic data transfer object for serializing/deserializing decision tables
- Expression language modules: CEL, FEEL, and json-logic support for evaluating expressions at runtime
DecisionTableDTO
The DecisionTableDTO is the serialization format for decision tables. It stores expressions as plain strings that are interpreted by the chosen expression language at load time.
"org.business4s" %% "decisions4s-persistence-core" % "0.1.2"
import decisions4s.persistence.DecisionTableDTO
import io.circe.parser.decode
val json = """
{
"name": "pricing",
"rules": [
{
"inputs": { "price": "> 100", "quantity": ">= 10" },
"outputs": { "discount": "0.1" },
"annotation": "Bulk discount"
},
{
"inputs": { "price": "> 0", "quantity": "> 0" },
"outputs": { "discount": "0.0" },
"annotation": "No discount"
}
]
}
"""
val dto: DecisionTableDTO = decode[DecisionTableDTO](json).toTry.get
CEL (Common Expression Language)
CEL is a fast, safe expression language developed by Google. It's widely used in cloud-native systems (Kubernetes, Envoy, etc.).
"org.business4s" %% "decisions4s-cel" % "0.1.2"
import decisions4s.*
import decisions4s.persistence.cel.*
case class PricingInput[F[_]](price: F[Int], quantity: F[Int]) derives HKD
case class PricingOutput[F[_]](discount: F[Double]) derives HKD
val celDto = DecisionTableDTO(
Seq(
DecisionTableDTO.Rule(
Map("price" -> "price > 100", "quantity" -> "quantity >= 10"),
Map("discount" -> "0.1"),
Some("Bulk discount"),
),
DecisionTableDTO.Rule(
Map("price" -> "true", "quantity" -> "true"),
Map("discount" -> "0.0"),
Some("No discount"),
),
),
"pricing",
)
val celTable = CelDecisionTable
.load(
celDto,
HKD.gatherGivens[PricingInput, ToCelType], // required to declare input variables
HKD.gatherGivens[PricingOutput, FromCel], // required to extract output variables
HitPolicy.First,
)
.get
val celResult = celTable.evaluateFirst(PricingInput(150, 20))
// celResult.output == Some(PricingOutput[Value](0.1))
FEEL (Friendly Enough Expression Language)
FEEL is the expression language defined by the DMN (Decision Model and Notation) standard. It's the native language for decision tables and is widely used in business process management.
"org.business4s" %% "decisions4s-feel" % "0.1.2"
import decisions4s.persistence.feel.*
val feelDto = DecisionTableDTO(
Seq(
DecisionTableDTO.Rule(
// FEEL unary tests - input value referenced via ?
Map("price" -> "> 100", "quantity" -> ">= 10"),
Map("discount" -> "0.1"),
Some("Bulk discount"),
),
DecisionTableDTO.Rule(
Map("price" -> "> 0", "quantity" -> "> 0"),
Map("discount" -> "0.0"),
Some("No discount"),
),
),
"pricing",
)
val feelTable = FeelDecisionTable
.load[PricingInput, PricingOutput, HitPolicy.First](
feelDto,
HKD.gatherGivens[PricingOutput, FromFeel], // required to extract output variables
HitPolicy.First,
)
.get
val feelResult = feelTable.evaluateFirst(PricingInput(150, 20))
// feelResult.output == Some(PricingOutput[Value](0.1))
json-logic
json-logic is a JSON-based rule format that enables sharing logic between frontend and backend code. Rules are expressed as JSON objects, making them easy to store in databases and transmit over APIs.
"org.business4s" %% "decisions4s-json-logic" % "0.1.2"
import decisions4s.persistence.jsonlogic.*
val jsonLogicDto = DecisionTableDTO(
Seq(
DecisionTableDTO.Rule(
// json-logic uses JSON objects for expressions
Map(
"price" -> """{">":[{"var":"price"}, 100]}""",
"quantity" -> """{">=":[{"var":"quantity"}, 10]}""",
),
Map("discount" -> "0.1"),
Some("Bulk discount"),
),
DecisionTableDTO.Rule(
Map(
"price" -> """{">":[{"var":"price"}, 0]}""",
"quantity" -> """{">":[{"var":"quantity"}, 0]}""",
),
Map("discount" -> "0.0"),
Some("No discount"),
),
),
"pricing",
)
val jsonLogicTable = JsonLogicDecisionTable
.load[PricingInput, PricingOutput, HitPolicy.First](
jsonLogicDto,
HKD.gatherGivens[PricingOutput, FromJsonLogic], // required to extract output variables
HitPolicy.First,
)
.get
val jsonLogicResult = jsonLogicTable.evaluateFirst(PricingInput(150, 20))
// jsonLogicResult.output == Some(PricingOutput[Value](0.1))
Choosing an Expression Language
| Aspect | CEL | FEEL | json-logic |
|---|---|---|---|
| Best for | Cloud-native, microservices | Business rules, DMN tooling | Frontend/backend sharing |
| Syntax | C-like, familiar to developers | DMN standard, business-friendly | JSON objects, fully structured |
| Unary tests | Full expressions required | Native support (> 100, [1..10]) | Full expressions required |
| Typing | Static (compile-time type checking) | Dynamic (runtime) | Dynamic (runtime) |