TinyRuleKit
Core Concepts
TinyRuleKit separates serialized rule data from executable Java behavior. JSON defines what should happen; Java resolvers define how each condition and action is compiled.
Rule
A rule has:
trigger: token used when firing the engine.id: optional diagnostic name.enabled: optional flag, defaulttrue.conditions: required list with at least one condition.actions: required list with at least one action.
Disabled rules are ignored before compilation, so their resolvers are not called and they are not indexed.
{
"id": "join_reward",
"enabled": true,
"trigger": "PLAYER_JOINED",
"conditions": [
{
"type": "LEVEL_AT_LEAST",
"params": {
"level": 5
}
}
],
"actions": [
{
"type": "GIVE_COINS",
"params": {
"amount": 25
}
}
]
}
An enabled rule without at least one condition and one action fails during engine compilation. A rule with
"enabled": false is skipped before this validation.
RuleContext
RuleContext is the runtime object passed to engine.fire(...). It should contain
the domain data that conditions and actions need for one trigger execution:
record GameContext(Player player, Wallet wallet) implements RuleContext {
@Override
public RuleVariables ruleVariables() {
return new RuleVariables()
.put("player", player)
.put("wallet", wallet);
}
}
Static conditions and actions can read the typed context directly:
(context, variables) -> context.player().level() >= level
ruleVariables() exposes named values for expression conditions and action guards. TinyRuleKit
creates those variables lazily during fire, so static rule sets that never read
variables do not pay the cost of materializing them.
Keep the context focused on data for the current trigger execution. Services, repositories, and infrastructure should usually stay behind compiled Java conditions or actions instead of being exposed through variables.
Precompilation
RuleEngine<GameContext> engine = RuleEngine.builder(GameContext.class)
.conditions(GameRules::condition)
.actions(GameRules::action)
.buildFromJson(json);
At runtime, rules are cached by trigger, so fire only scans rules registered for the requested
trigger.
Token Enums
Rule files use strings because they are JSON, but the application should expose those strings through enum collections:
enum GameTrigger implements RuleToken {
PLAYER_JOINED,
QUEST_COMPLETED
}
enum GameCondition implements RuleToken {
LEVEL_AT_LEAST,
HAS_PERMISSION
}
enum GameAction implements RuleToken {
GIVE_COINS,
SEND_MESSAGE
}
This keeps the rule vocabulary explicit and avoids consumer code that switches directly on raw strings.
Resolver Factories
Resolver factories turn serialized rule definitions into executable Java functions. A condition factory compiles a condition token:
static CompiledCondition<GameContext> condition(String type, RuleParams params) {
return switch (RuleToken.enumValue(GameCondition.class, type)) {
case LEVEL_AT_LEAST -> {
int level = params.getInt("level");
yield (context, variables) -> context.player().level() >= level;
}
case HAS_PERMISSION -> {
String permission = params.getString("permission");
yield (context, variables) -> context.player().hasPermission(permission);
}
};
}
An action factory compiles an action token:
static CompiledAction<GameContext> action(String type, RuleParams params) {
return switch (RuleToken.enumValue(GameAction.class, type)) {
case GIVE_COINS -> {
int amount = params.getInt("amount", 1);
yield (context, variables) -> context.wallet().addCoins(amount);
}
case SEND_MESSAGE -> {
String message = params.getString("message");
yield (context, variables) -> context.messages().send(message);
}
};
}
Factories run when the engine is built. Prefer doing validation and parameter conversion there so runtime execution stays simple.