After years working on distributed systems under tight specification constraints, I find myself free to explore ideas purely out of curiosity — no deadlines, no stakeholders, no “we can’t do that in production.” This is the second incursion after saga-graph.
saga-blind is a runtime that accepts .saga definition files dropped into a watched folder and executes them without restarting. Steps live in a jar declared in the definition. The engine loads the jar at launch time, resolves parameter mappings from an OKV pool, and calls the step providers. It knows nothing about what the steps do.
saga: goblin-recruit
jar: ./libs/goblin-services.jar
steps:
- id: measurements
kind: mandatory
class: sagablind.goblin.MeasurementsService
inputs:
- param: goblinId
from: __init__/goblinId
- parallel:
- id: smithy
kind: mandatory
class: sagablind.goblin.SmithyService
inputs:
- param: armLength
from: measurements/armLength
compensate:
- param: weaponId
from: smithy/weaponId
- id: enlist
kind: mandatory
class: sagablind.goblin.EnlistService
inputs:
- param: weaponType
from: smithy/weaponType
Every state transition is written to a WAL before the action it describes. If the process dies mid-execution, a ZombieHunter agent reconstructs the pool and the jar from the WAL and retries forward — or compensates LIFO if the retry fails.
The jar contract is minimal: execute(args: Map[String, ujson.Value]) and compensate(args: Map[String, ujson.Value]). The jar receives named values and returns named outputs. It knows nothing about the pool, the WAL, or the orchestration around it.
Scala 3 · SQLite · Cask · no framework dependencies in core.
Source, architecture diagrams and demo videos: GitHub - ccerdadiaz/saga-blind · GitHub
Feedback welcome.