A common architecture for business logic is defined by BPMN, and a well known implementation is provided by Camunda. But BPMN has a general purpose target, so it is oversized for a specific problem.
As a SaaS provider, you want to:
- Serve more customer
- Charge each customer for the service
- Provide reports about your service (expose why you are charging your customer).
- Scale number of customer in a effective way.
- Warrant security and privacy for each customer (separation)
Sometime your service is based on API provided by third party, so you must conform to their policy.
This is the case of Amazon Selling Partner API, but not limited to them. Even if you are monitoring, or providing a penetration test service for your customer, you need to respect some kind of policy for enquiring external API calls.
My solution involved the use of arbiter architecture pattern (somewhere referred as Saga Pattern)
Key features:
- Digraph based transition between steps
- Step specific execution policy: throttled, group throttled, immediate
- Step specific out_degree: multiple path allowed
- Path selection logic implementation delegated to worker
- Backup task payload & execution on Database
- Task metadata managed by Message Broker (RabbitMQ)
- Consumer/worker agnostic about policy: it just run the task and select the path to follow
- Digraph analysis and acceptance: deny graph cycle
- Expose API with details about workload of each step (a graph node), in dot format (graphviz)
- Control & Command: pause/resume/flush individual workload for each node
- Execution path managed by the Arbiter
- Worker monitoring via instrumenting (exports opentelemetry metrics)
- Scale worker number effectively: as many as required, based on number of task, not number of customer
- Arbiter monitoring by opentelemetry
- Global report (system level for administrator)
- Customer report (in a dedicated step handled by a worker)
From the developer point of view:
- Focus on the specific task: just fetch a single message from Message Broker, execute payload, transite to next node/task.
- Simple module provided for handling MessageBroker message consumption/acknowledge (nodejs, golang)



Focus on protocol, then implement features
This project, the architecture design, the arbiter dealing with transition command, and the real services consuming tasks, is guided by requirement, but also it was a time constrained work: I need to release it in few weeks. The first idea to adopt NodeJS reduced the stability, but since the protocol and the format of communication is based on JSON payload, with a well defined JSON-Schema, it was possible to switch to Golang improve reliability, and implement full feature set.
I used to work as a Front-end developer too, till 2020, a good advice about Frontend is: focus on the GUI, then it comes design patterns, then the code.
On backend side, the architecture of the service and its specific features and capabilities come first, but the protocol represents the “interface between actors”, and this is strictly coupled with the architectural choice.
The Message Broker choice is guided by features list.
Anyway on cloud native world, nothing is designed to stay the same forever: when it makes sense, the architecture can change, and a change plan can be defined also for consumers (by moving one by one, or only some of those).
Conclusion
This project caused me some problem during my off days, when I had to inspect Kibana messages, restart services from Swarmpit, commit changes from Gitea web-interfaces, and all this from my smartphone (small screen, big deal).