I’m looking for guidance on the design concept for an application we are building using Elixir/Ecto/Phoenix* which uses a rule-based system that observes the behaviour of a group of users and interacts with them.
Our application will have a number of groups (think management team or board members) and will build a facts-base from the data provided by a group and its activities. A set of rules will then be used to drive some application behaviours based on the data.
A rules-engine approach seems very fit to this kind of activity (taking into account the warnings of people like Martin Fowler about the complexity of such systems).
In thinking about how to design it I am gravitating towards the idea that we should use a GenServer with the facts database of each group as it’s state. That is rather than having one big monolithic GenServer as a rules processor for the entire DB, we create a separate GenServer instance for each group.
I see a number of advantages:
-
Each GenServer will be smaller, having only the data for one group to turn into its facts base.
-
Each GenServers could be distributed to reduce individual host load (esp. if we used something like fly.io for the app)
-
We can use Phoenix pub/sub to glue everything together so that the app and rules engine can communicate
-
It keeps us all one stack BEAM/Elixir/… so reduces the required knowledge surface area of the team
Three issues are on my mind:
- Building a rules engine
Building a “naive” rules engine isn’t especially difficult. But is is more code to implement. There are not, so far as I can see, any released native BEAM rules engines although projects like Bob Sollish’s Rules_Engine and the Retex library are potentially useful starting points.
Of course we could use something like DROOLS or JESS which are big established players. I’m not keen (a) because I would like to stay on the BEAM platform and not introduce another and (b) because they are designed for big complex organisations with big complex rules and it seems to me that big complex rules is a rules-base smell.
This latter part is quite important as big complex rules systems employing chaining can be hard to reason about. I’d almost take the “naive” point as a useful constraint.
- How to orchestrate lots of GenServers
I am not sure how to orchestrate all these various GenServers. For example if we have hundreds of groups, what is reponsible for saying “Oh, there’s a new group here that needs a rules-engine, go spin up a GenServer for them”. If the app is restarted, what makes sure everything comes up properly?
- Preservation of state
How to preserve the state of the GenServer fact-base itself. The facts will, ultimately, come from the application database through a process of translation. But this could be unwieldy for startup times as the fact-base gets bigger. Maybe we don’t need to consider this now but it could be an issue long-term.
This is my first foray into the design of a (albeit modestly) complex Elixir application and I would be grateful for any thoughts in response to what I’ve written, suggestions for people/things to read or think about, or suggestions more directly about what we are attempting.
Many thanks.
Matt