Skip to main content

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.

Experimental

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

AspectCELFEELjson-logic
Best forCloud-native, microservicesBusiness rules, DMN toolingFrontend/backend sharing
SyntaxC-like, familiar to developersDMN standard, business-friendlyJSON objects, fully structured
Unary testsFull expressions requiredNative support (> 100, [1..10])Full expressions required
TypingStatic (compile-time type checking)Dynamic (runtime)Dynamic (runtime)