I don't know anything about ODM, so I can't comment about the similarities/differences. But from what is described in the question about the use case, I think the problem is that you're using Drools wrong. :)
Generally speaking, we'd invoke rules like this:
KieContainer kieContainer = ...
KieBase rules = this.kieContainer.getKieBase("validation"); // call the validation rules
KieSession session = rules.newKieSession();
try {
session.insert( ... ); // add facts into working memory
session.fireAllRules();
} finally {
session.dispose();
}
fireAllRules
is a synchronous, blocking method.
Alternatively, if you want to do batch processing, you'd do something like this (example taken from the documentation):
StatelessKieSession ksession = kbase.newStatelessKieSession();
List cmds = new ArrayList();
cmds.add( CommandFactory.newInsertObject( new Cheese( "stilton", 1), "stilton") );
cmds.add( CommandFactory.newStartProcess( "process cheeses" ) );
cmds.add( CommandFactory.newQuery( "cheeses" ) );
ExecutionResults bresults = ksession.execute( CommandFactory.newBatchExecution( cmds ) );
Cheese stilton = ( Cheese ) bresults.getValue( "stilton" );
QueryResults qresults = ( QueryResults ) bresults.getValue( "cheeses" );
Notice we call startProcess
in the list of commands, but not directly. Further we still wait for the results at the execute
call, so while startProcess
does proceed asynchronously, we still block for all executions to complete before continuing at execute
.
Your stated use case was:
The rules, if condition is met, builds a set of output data that the Spring app uses to behave differently based on the result.
There's nothing here that precludes you from using Drools. At my previous company, we had a set of rules that actually routed requests between services based on the state of the data in the request, including modifying the request in flight. We also had rules that could do normal processing like validation, calculation, and so on.
Your model depth is also not really a hindrance -- you can write rules against your model no matter how it's structured. There are some types which are inherently inferior (please don't pass Map
s into the rules) but generally if it's in working memory you can work with it.
Finally, processing time is a factor mostly of how well written your rules are. There are structures in Drools that allow you to modify working memory and rerun the rules -- this will cause them to take inherently more time because (duh) you're running the rules a second time! But a simple fall-through sort of rules -- trigger which ever ones trigger and keep moving -- those can be very fast. At my last job I had a single service with 1.4 million rules that had a sub-second SLA; it was so fast because there were no 'update' calls, and none of the rules cared about which of the other rules also fired. The rules were effectively stateless, and the worst-case performance was 300 milliseconds (average was 30 ms; and that's round trip through the entire spring-boot app and not just the actual rule evaluation.)
For rules that depend on complex data objects many levels deep in a POJO, how would you derive that data and not causing rules to care about other rules? Let's say my object structure is something like Person -> Vehicle -> Make -> Model -> Options. If I'm running rules specifically on "Options", how are getting to the "model" data in the Options rules? What if model or make is empty/null to block Options from running?
Parsing out the example, we have these models (assume getters and setters on all fields):
class Options {}
class Model {
private Options options;
}
class Make {
private Model model;
}
class Vehicle {
private Make make;
}
class Person {
private Vehicle vehicle;
}
To get to the 'options', we just pull them out of the intermediate models. Assuming you put a Person
instance into working memory:
rule "Options"
when
// this is the Person in working memory; get the vehicle out.
Person( $v: vehicle )
// then drill down to Options by repeating the process
Vehicle( $make: make ) from $v
Make( $model: model ) from $make
Model( $opts: options ) from $model
// now we can do stuff with $opts
then
// $opts is available for use here too
end
Drools is generally pretty good at doing defensive 'gets', but you can be defensive in your own right by adding a null check. For example:
rule "Options with explicit null checks"
when
Person( $v: vehicle != null )
Vehicle( $make: make != null ) from $v
Make( $model: model != null ) from $make
Model( $opts: options != null ) from $model
then
// ...
end
Alternatively if you want to fail fast and just discard any inputs with bad data, you can retract
them from working memory. This is potentially dangerous because it's a premature exit from the workflow, and can cause potential bugs downstream if you don't realize it's there. (It's somewhat analogous to having multiple "return" statements in your Java code; if you're debugging down near the bottom of a method and aren't aware that there's an early return much farther up, that might cause you to waste time or introduce unexpected behavior.)
rule "Retract incomplete date"
salience 100
when
$person: Person ($v: vehicle != null)
// as an example, this rule is going to trap for missing Make
Vehicle( make == null ) from $v
then
retract($person);
end
Word of caution: once you call retract
, that item is gone from working memory, so if any subsequent rules were relying on it, they'll no longer fire.
The salience isn't necessary but historically I've found it easier to mentally track early retractions like this when they're explicitly called out as happening earlier than the regular rules. Generally speaking it's better practice to simply write rules that are mutually exclusive, but you may find marginally better performance using an early retract.