KFeel

KFeel is a zero-dependency Kotlin library designed for mission-critical business rule evaluation in financial and enterprise systems. Built for DMN 1.3 FEEL specification compliance with zero-error-tolerance requirements.

Core Features

ðŸŽŊ Expression Evaluation Engine

Parse and evaluate DMN FEEL expressions with full specification compliance.

  • Immutable expression trees compiled once, evaluated millions of times

  • Type-safe value system with sealed class hierarchy

  • Three-valued logic with proper null propagation semantics

  • Complete DMN 1.3 operator precedence and evaluation rules

See ca.acendas.kfeel.api/-feel-expression/index.html for full API documentation.

📊 62 Built-in Functions

Complete DMN 1.3 FEEL function library for business logic.

  • Numeric functions - abs, ceiling, floor, round, sqrt, modulo, even, odd

  • String functions - substring, matches, replace, split, upper/lower case

  • List functions - sum, mean, count, sort, filter, flatten, distinct values

  • Temporal functions - date/time arithmetic, day of week, month of year

  • Type conversion - string, number, date, time, duration with format control

See ca.acendas.kfeel.evaluator/-built-in-functions/index.html for complete reference.

🔄 Dynamic Context Management

Runtime variable resolution with nested scope support.

  • Immutable contexts with copy-on-write semantics

  • Path navigation for structured data (e.g., customer.address.city)

  • List filtering with FEEL predicates

  • Type-safe variable access with compile-time guarantees

See ca.acendas.kfeel.api/-feel-context/index.html for context management API.

⚡ High-Performance Parsing

Optimized lexer and parser for sub-millisecond compilation.

  • O(n) tokenization with position tracking for error reporting

  • Recursive descent parser with lookahead optimization

  • AST caching for repeated expression evaluation

  • Thread-safe compiled expressions for concurrent workloads

See ca.acendas.kfeel.parser/-feel-parser/index.html and ca.acendas.kfeel.parser/-feel-lexer/index.html.

Why Choose KFeel?

Zero Dependencies, Maximum Reliability

  • Only Kotlin stdlib required - no external framework dependencies

  • Minimal attack surface for security-critical environments

  • Small runtime footprint - suitable for embedded systems and serverless

  • No version conflict hell - drop into any Kotlin project without issues

Financial-Grade Safety

  • 100% DMN 1.3 spec compliance - all 62 built-in functions fully implemented

  • Arbitrary precision arithmetic with BigDecimal for financial calculations

  • 88% test coverage with 450+ comprehensive tests including edge cases

  • Structured exception hierarchy for granular error handling

  • Immutable data structures eliminating entire classes of concurrency bugs

Superior Developer Experience

  • Intuitive API - parse once, evaluate many times with clear semantics

  • Type-safe evaluation with evaluateAs<T>() for compile-time guarantees

  • Comprehensive KDoc - every public API fully documented with examples

  • Clear error messages with source position tracking (line, column, index)

  • REPL-friendly - evaluate expressions interactively for rapid prototyping

Quick Start Examples

Requirements

  • JDK 17+ (LTS recommended)

  • Kotlin 2.1.0+

Installation

// Gradle (Kotlin DSL)
dependencies {
implementation("ca.acendas:kfeel:1.0.0")
}
<!-- Maven -->
<dependency>
<groupId>ca.acendas</groupId>
<artifactId>kfeel</artifactId>
<version>1.0.0</version>
</dependency>

Simple Expression Evaluation

import ca.acendas.kfeel.api.*

// Parse and evaluate arithmetic
val result = FeelExpression.parse("2 + 3 * 4").evaluate()
println(result.toKotlin()) // → 14

// Boolean expressions
val approved = FeelExpression.parse("age >= 18 and hasId").evaluate(
FeelContext.of("age" to 25, "hasId" to true)
)
println(approved.toKotlin()) // → true

Business Rules Engine

Complete example implementing dynamic pricing logic with tiered discounts:

import ca.acendas.kfeel.api.*

// Define pricing rule with nested conditionals
val pricingRule = FeelExpression.parse("""
if customer_tier = "platinum" and order_amount >= 1000 then 0.25
else if customer_tier = "gold" and order_amount >= 1000 then 0.20
else if customer_tier = "gold" and order_amount >= 500 then 0.15
else if customer_tier = "silver" and order_amount >= 500 then 0.10
else if customer_tier = "silver" and order_amount >= 200 then 0.05
else if is_member then 0.03
else 0.0
""")

// Create order context
val orderContext = FeelContext.of(
"customer_tier" to "gold",
"order_amount" to 1500,
"is_member" to true
)

// Calculate discount
val discount = pricingRule.evaluateAs<Number>(orderContext)?.toDouble() ?: 0.0
println("Discount rate: ${discount * 100}%") // → Discount rate: 20.0%

// Calculate final price
val finalPrice = 1500 * (1 - discount)
println("Final price: $$finalPrice") // → Final price: $1200.0

Key Features Demonstrated:

  • Complex nested conditionals with multiple tiers

  • Type-safe evaluation with evaluateAs<Number>()

  • Context-based variable resolution

  • Financial calculations with proper decimal handling

Decision Table Implementation

import ca.acendas.kfeel.api.*

// Credit risk assessment decision table
val creditRiskRule = FeelExpression.parse("""
if credit_score >= 750 and debt_ratio < 0.30 then "approved"
else if credit_score >= 700 and debt_ratio < 0.40 and employment_years >= 2 then "approved"
else if credit_score >= 650 and debt_ratio < 0.35 and employment_years >= 5 then "manual_review"
else if credit_score >= 600 and debt_ratio < 0.30 and employment_years >= 10 then "manual_review"
else "declined"
""")

val applicant = FeelContext.of(
"credit_score" to 720,
"debt_ratio" to 0.28,
"employment_years" to 5
)

val decision = creditRiskRule.evaluateAs<String>(applicant) ?: "declined"
println("Application status: $decision") // → Application status: approved

List Processing and Aggregation

import ca.acendas.kfeel.api.*

// Sales data analysis
val salesContext = FeelContext.of(
"sales" to listOf(
mapOf("region" to "North", "amount" to 15000, "product" to "Widget-A"),
mapOf("region" to "South", "amount" to 22000, "product" to "Widget-B"),
mapOf("region" to "North", "amount" to 18000, "product" to "Widget-C"),
mapOf("region" to "South", "amount" to 31000, "product" to "Widget-A"),
mapOf("region" to "East", "amount" to 12000, "product" to "Widget-B")
)
)

// Filter and aggregate by region
val northTotal = FeelExpression.parse(
"""sum(sales[item.region = "North"].amount)"""
).evaluate(salesContext)
println("North region total: ${northTotal.toKotlin()}") // → 33000

// Get top-selling regions
val topRegions = FeelExpression.parse("""
sort(
distinct values(sales.region),
function(a, b) sum(sales[item.region = a].amount) > sum(sales[item.region = b].amount)
)
""").evaluate(salesContext)
println("Top regions: ${topRegions.toKotlin()}")

Temporal Calculations

import ca.acendas.kfeel.api.*
import java.time.LocalDate

// Age verification and date arithmetic
val context = FeelContext.of(
"birthdate" to LocalDate.of(1990, 5, 15),
"hire_date" to LocalDate.of(2020, 3, 1)
)

// Calculate age
val age = FeelExpression.parse("year(today()) - year(birthdate)").evaluate(context)
println("Age: ${age.toKotlin()}")

// Calculate years of service
val yearsOfService = FeelExpression.parse(
"year(today()) - year(hire_date)"
).evaluate(context)
println("Years of service: ${yearsOfService.toKotlin()}")

// Day of week for a date
val hireDayOfWeek = FeelExpression.parse(
"day of week(hire_date)"
).evaluate(context)
println("Hired on: ${hireDayOfWeek.toKotlin()}") // → "sunday"

Main API Entry Points

Primary interfaces for FEEL expression evaluation:

  • ca.acendas.kfeel.api/-feel-expression/parse.html - Parse FEEL expression string into compiled expression

  • ca.acendas.kfeel.api/-feel-expression/evaluate.html - Evaluate expression with optional context

  • ca.acendas.kfeel.api/-feel-expression/evaluate-as.html - Type-safe evaluation with Kotlin type casting

  • ca.acendas.kfeel.api/-feel-context/of.html - Create evaluation context with variables

  • ca.acendas.kfeel.api/-feel-value/to-kotlin.html - Convert FEEL values to Kotlin types

  • ca.acendas.kfeel.api/-feel-value/from.html - Create FEEL values from Kotlin objects

See complete API documentation in ca.acendas.kfeel.api/index.html.

Architecture & Concepts

Three-Stage Compilation Pipeline

KFeel follows a classic compiler architecture optimized for repeated evaluation:

1. Lexical Analysis (ca.acendas.kfeel.parser/-feel-lexer/index.html)

Converts raw FEEL expression strings into token streams:

  • Input: "2 + 3 * 4"

  • Output: [NUMBER(2), PLUS, NUMBER(3), STAR, NUMBER(4), EOF]

  • O(n) time complexity with position tracking for error reporting

  • Handles keywords (case-sensitive), operators, literals, and delimiters

2. Syntax Parsing (ca.acendas.kfeel.parser/-feel-parser/index.html)

Builds immutable Abstract Syntax Trees respecting FEEL operator precedence:

  • Input: [NUMBER(2), PLUS, NUMBER(3), STAR, NUMBER(4), EOF]

  • Output: BinaryOpNode(+, NumberNode(2), BinaryOpNode(*, NumberNode(3), NumberNode(4)))

  • Recursive descent parser with predictive parsing

  • Produces position-annotated AST for runtime error reporting

3. Runtime Evaluation (ca.acendas.kfeel.evaluator/-feel-evaluator/index.html)

Traverses AST and evaluates nodes with runtime context:

  • Input: AST + FeelContext

  • Output: FeelValue (typed result)

  • Implements three-valued logic (true, false, null)

  • Null propagation through operations

  • Short-circuit evaluation for AND/OR

Type System

FEEL uses a rich type system mapped to Kotlin types:

FEEL TypeKotlin TypeFeelValue ClassExample
numberBigDecimalFeelValue.Number42, 3.14159
stringStringFeelValue.Text'hello world'
booleanBooleanFeelValue.Booleantrue, false
dateLocalDateFeelValue.Datedate("2025-01-15")
timeLocalTimeFeelValue.Timetime("14:30:00")
date and timeZonedDateTimeFeelValue.DateTimenow()
durationPeriod/DurationFeelValue.Durationduration("P1Y2M")
listList<FeelValue>FeelValue.List[1, 2, 3]
contextMap<String, FeelValue>FeelValue.Context{x: 1, y: 2}
null-FeelValue.Nullnull

Type Checking:

val expr = FeelExpression.parse("x instance of number")
val result = expr.evaluate(FeelContext.of("x" to 42)) // → true

Supported type names: number, string, boolean, date, time, date and time, duration, list, context

Three-Valued Logic

FEEL implements three-valued logic where operations can return true, false, or null:

// Null propagation
val ctx = FeelContext.of("x" to null)
FeelExpression.parse("x + 5").evaluate(ctx) // → null
FeelExpression.parse("x > 10").evaluate(ctx) // → null

// Short-circuit evaluation
FeelExpression.parse("false and x").evaluate(ctx) // → false (x not evaluated)
FeelExpression.parse("true or x").evaluate(ctx) // → true (x not evaluated)

Production Readiness

KFeel is built for zero-error-tolerance environments with financial-grade reliability:

Performance Characteristics:

  • >10,000 expressions/second evaluation throughput on modern hardware

  • <1ms parsing latency for typical business rules (50-200 tokens)

  • <100Ξs evaluation latency for simple expressions

  • Thread-safe compiled expressions - parse once, evaluate concurrently

  • Minimal memory footprint - ~50-100 bytes per AST node

Reliability Guarantees:

  • 88% test coverage with 450+ comprehensive test cases

  • 100% DMN 1.3 compliance - all 62 built-in functions fully tested

  • Immutable data structures - no shared mutable state

  • Structured exception hierarchy - granular error handling

  • Arbitrary precision arithmetic - BigDecimal for financial calculations

Thread Safety:

  • FeelExpression instances are immutable and thread-safe after parsing

  • FeelContext instances are mutable and NOT thread-safe - use one per thread

  • FeelValue instances are immutable and thread-safe

  • Built-in functions are stateless and thread-safe

Error Handling:

try {
val expr = FeelExpression.parse("invalid + syntax")
expr.evaluate()
} catch (e: FeelParseException) {
// Syntax error at position (line, column, index)
println("Parse error at ${e.position}: ${e.message}")
} catch (e: FeelTypeException) {
// Type mismatch (e.g., "hello" + 5)
println("Type error: expected ${e.expected}, got ${e.actual}")
} catch (e: FeelUndefinedVariableException) {
// Variable not found in context
println("Undefined variable: ${e.variableName}")
} catch (e: FeelEvaluationException) {
// Runtime error (division by zero, list index out of bounds, etc.)
println("Evaluation error: ${e.message}")
}

Built-in Functions Reference

KFeel implements all 62 DMN 1.3 FEEL built-in functions organized by category.

Numeric Functions (8)

Mathematical operations with arbitrary precision.

FunctionDescriptionExample
abs(n)Absolute valueabs(-5) → 5
ceiling(n)Round up to integerceiling(3.2) → 4
floor(n)Round down to integerfloor(3.8) → 3
round(n, scale)Round to scale decimalsround(3.567, 2) → 3.57
sqrt(n)Square rootsqrt(16) → 4
even(n)True if eveneven(4) → true
odd(n)True if oddodd(5) → true
modulo(dividend, divisor)Remaindermodulo(10, 3) → 1

String Functions (12)

Text processing and manipulation with regex support.

FunctionDescriptionExample
string length(s)Length of stringstring length("hello") → 5
upper case(s)Convert to uppercaseupper case("hello") → "HELLO"
lower case(s)Convert to lowercaselower case("HELLO") → "hello"
substring(s, start, len?)Extract substring (1-based)substring("hello", 2, 3) → "ell"
substring before(s, match)Before first matchsubstring before("hello world", " ") → "hello"
substring after(s, match)After first matchsubstring after("hello world", " ") → "world"
contains(s, match)True if containscontains("hello", "ell") → true
starts with(s, prefix)True if starts withstarts with("hello", "he") → true
ends with(s, suffix)True if ends withends with("hello", "lo") → true
matches(s, pattern)Regex matchmatches("abc", "[a-z]+") → true
replace(s, pattern, repl, flags?)Regex replacereplace("hello", "l", "r") → "herro"
split(s, delimiter)Split into listsplit("a,b,c", ",") → ["a", "b", "c"]

List Functions (18)

Collection processing and aggregation.

FunctionDescriptionExample
count(list)Number of elementscount([1, 2, 3]) → 3
sum(list)Sum of numberssum([1, 2, 3]) → 6
mean(list)Average valuemean([1, 2, 3]) → 2
min(list) or min(a, b, ...)Minimum valuemin([3, 1, 2]) → 1
max(list) or max(a, b, ...)Maximum valuemax([3, 1, 2]) → 3
append(list, item...)Add to endappend([1, 2], 3) → [1, 2, 3]
concatenate(list...)Merge listsconcatenate([1, 2], [3, 4]) → [1, 2, 3, 4]
distinct values(list)Remove duplicatesdistinct values([1, 2, 2, 3]) → [1, 2, 3]
flatten(list)Flatten nested listsflatten([[1, 2], [3, 4]]) → [1, 2, 3, 4]
sort(list, precedes?)Sort listsort([3, 1, 2]) → [1, 2, 3]
reverse(list)Reverse orderreverse([1, 2, 3]) → [3, 2, 1]
sublist(list, start, len?)Extract sublist (1-based)sublist([1, 2, 3, 4], 2, 2) → [2, 3]
insert before(list, pos, item)Insert at positioninsert before([1, 3], 2, 2) → [1, 2, 3]
remove(list, pos)Remove at positionremove([1, 2, 3], 2) → [1, 3]
index of(list, match)Find positionsindex of([1, 2, 3, 2], 2) → [2, 4]
union(list...)Merge with uniqueunion([1, 2], [2, 3]) → [1, 2, 3]
all(list) or all(b...)True if all trueall([true, true]) → true
any(list) or any(b...)True if any trueany([false, true]) → true

Temporal Functions (12)

Date, time, and duration operations.

FunctionDescriptionExample
now()Current date-timenow() → 2025-01-15T14:30:25Z
today()Current datetoday() → 2025-01-15
year(d)Extract yearyear(date("2025-01-15")) → 2025
month(d)Extract monthmonth(date("2025-01-15")) → 1
day(d)Extract dayday(date("2025-01-15")) → 15
hour(t)Extract hourhour(time("14:30:00")) → 14
minute(t)Extract minuteminute(time("14:30:00")) → 30
second(t)Extract secondsecond(time("14:30:25")) → 25
day of week(d)Day name (lowercase)day of week(date("2025-01-15")) → "wednesday"
month of year(d)Month name (lowercase)month of year(date("2025-01-15")) → "january"
week of year(d)ISO week numberweek of year(date("2025-01-15")) → 3
day of year(d)Day number in yearday of year(date("2025-01-15")) → 15

Context Functions (2)

Working with structured data.

FunctionDescriptionExample
get entries(ctx)Get key-value pairsget entries({a: 1, b: 2}) → [{key: "a", value: 1}, {key: "b", value: 2}]
get value(ctx, key)Get value by keyget value({a: 1, b: 2}, "a") → 1

Conversion Functions (7)

Type conversion and formatting.

FunctionDescriptionExample
string(x)Convert to stringstring(123) → "123"
number(s, grp?, dec?)Parse numbernumber("123.45") → 123.45
date(s) or date(y,m,d)Create datedate("2025-01-15") → 2025-01-15
time(s) or time(h,m,s,offset?)Create timetime("14:30:00") → 14:30:00
date and time(d, t) or date and time(s)Create datetimedate and time("2025-01-15T14:30:00Z")
duration(s)Parse durationduration("P1Y2M") → P1Y2M
decimal(n, scale)Round to decimalsdecimal(3.14159, 2) → 3.14

Boolean Functions (3)

Logical operations.

FunctionDescriptionExample
not(b)Logical NOTnot(true) → false
all(list) or all(b...)Logical AND of allall([true, true, false]) → false
any(list) or any(b...)Logical OR of anyany([false, false, true]) → true

Integration Patterns

Expression Caching for High-Performance Scenarios

import ca.acendas.kfeel.api.*
import java.util.concurrent.ConcurrentHashMap

class FeelExpressionCache {
private val cache = ConcurrentHashMap<String, FeelExpression>()

fun evaluate(expression: String, context: FeelContext): FeelValue {
val compiled = cache.getOrPut(expression) {
FeelExpression.parse(expression)
}
return compiled.evaluate(context)
}

fun clear() = cache.clear()
}

// Usage in high-throughput scenario
val expressionCache = FeelExpressionCache()

val results = orders.map { order ->
val ctx = FeelContext.of(
"amount" to order.amount,
"tier" to order.customerTier
)
expressionCache.evaluate("amount * (1 - discount_rate)", ctx)
}

Decision Service Pattern

import ca.acendas.kfeel.api.*

class DecisionService {
private val rules = mapOf(
"pricing" to FeelExpression.parse("""
if tier = "platinum" then 0.25
else if tier = "gold" then 0.15
else if tier = "silver" then 0.10
else 0.05
"""),
"shipping" to FeelExpression.parse("""
if amount >= 100 then 0.0
else if amount >= 50 then 5.99
else 9.99
""")
)

fun evaluate(ruleName: String, context: FeelContext): Any? {
val rule = rules[ruleName] ?: throw IllegalArgumentException("Unknown rule: $ruleName")
return rule.evaluate(context).toKotlin()
}
}

Next Steps

Learn More:

  • Explore package documentation: ca.acendas.kfeel.api/index.html, ca.acendas.kfeel.parser/index.html, ca.acendas.kfeel.evaluator/index.html

  • Review the complete DMN 1.3 FEEL specification

  • Check out the test suite for advanced examples

Get Help:

  • Report issues on GitHub

  • Read CONTRIBUTING.md for development guidelines

License

Copyright (c) 2025 ACENDAS GLOBAL INC.

This software is proprietary and confidential. Binary distribution is freely permitted for any use, including commercial applications. Source code remains proprietary and may not be modified or redistributed. See LICENSE for complete details.

Packages

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard