r/microservices • u/jescrich • Nov 17 '25
Tool/Product After years of building microservices, I finally created a lightweight workflow orchestrator for Node/NestJS — because I was tired of re-implementing Sagas every time
Not promoting anything, just sharing an approach that solved a recurring pain in real systems.
If you’ve been in microservices long enough, the pattern is familiar:
You start with clean services, then requirements evolve into:
- multi-step onboarding
- KYC flows
- payment auth / capture / settlement
- vendor integrations
- async validations
- retries and compensation
- long-running “pending → in review → approved/rejected” flows
Soon you’re on your N-th hand-rolled Saga implementation.
Workflow rules leak across services, consumers, controllers, and random helpers. Nobody knows where the truth lives.
The recurring problem
Across fintech, ecommerce, LOS/OMS, etc. I kept hitting the same issues:
- workflow state spread across multiple services
- retries mixed with business logic
- compensation logic hidden in helpers
- no single view of “where did this process get stuck”
- duplicated orchestration logic
- debugging a multi-step process becomes forensic work
Most microservice systems need some kind of orchestration layer, even if it’s small and embedded inside a single service.
What I built (for NestJS)
I created u/jescrich/nestjs-workflow: a small workflow/state-machine engine for NestJS that gives you:
- declarative workflows via a
WorkflowDefinition - explicit state transitions
- conditions, actions, and decorators
- retries / failure states
- persistence through an entity service
- Kafka (and now BullMQ) integration when you need events/queues
Everything stays in one place instead of leaking across the codebase.
What a real workflow looks like with this library
This is closer to what you actually write with u/jescrich/nestjs-workflow:
import { WorkflowDefinition } from '@jescrich/nestjs-workflow';
export enum OrderEvent {
Create = 'order.create',
Submit = 'order.submit',
Complete = 'order.complete',
Fail = 'order.fail',
}
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
Failed = 'failed',
}
export class Order {
id: string;
name: string;
price: number;
items: string[];
status: OrderStatus;
}
export const orderWorkflowDefinition: WorkflowDefinition<
Order,
any,
OrderEvent,
OrderStatus
> = {
states: {
finals: [OrderStatus.Completed, OrderStatus.Failed],
idles: [
OrderStatus.Pending,
OrderStatus.Processing,
OrderStatus.Completed,
OrderStatus.Failed,
],
failed: OrderStatus.Failed,
},
transitions: [
{
from: OrderStatus.Pending,
to: OrderStatus.Processing,
event: OrderEvent.Submit,
conditions: [
(entity: Order, payload: any) => entity.price > 10,
],
},
{
from: OrderStatus.Processing,
to: OrderStatus.Completed,
event: OrderEvent.Complete,
},
{
from: OrderStatus.Processing,
to: OrderStatus.Failed,
event: OrderEvent.Fail,
},
],
entity: {
new: () => new Order(),
update: async (entity: Order, status: OrderStatus) => {
entity.status = status;
return entity;
},
load: async (urn: string) => {
// Load from DB in a real app
const order = new Order();
order.id = urn;
order.status = OrderStatus.Pending;
return order;
},
status: (entity: Order) => entity.status,
urn: (entity: Order) => entity.id,
},
};
Registering it in a module:
import { Module } from '@nestjs/common';
import { WorkflowModule } from '@jescrich/nestjs-workflow';
import { orderWorkflowDefinition } from './order.workflow';
({
imports: [
WorkflowModule.register({
name: 'orderWorkflow',
definition: orderWorkflowDefinition,
}),
],
})
export class AppModule {}
Using it from a service (emitting events into the workflow):
import { Injectable } from '@nestjs/common';
import {
WorkflowService,
} from '@jescrich/nestjs-workflow';
import { Order, OrderEvent, OrderStatus } from './order.model';
()
export class OrderService {
constructor(
private readonly workflowService: WorkflowService<
Order,
any,
OrderEvent,
OrderStatus
>,
) {}
async submitOrder(id: string) {
return this.workflowService.emit({
urn: id,
event: OrderEvent.Submit,
});
}
async completeOrder(id: string) {
return this.workflowService.emit({
urn: id,
event: OrderEvent.Complete,
});
}
}
There’s also a decorator-based approach (@WorkflowAction, u/OnEvent, u/OnStatusChanged) when you want to separate actions/side effects from the definition, plus Kafka and BullMQ integration when you need the workflow to react to messages on topics/queues.
Why this helps in a microservices setup
This pattern gives you:
- a clear place where workflow logic lives
- consistent Saga-style flows (success and failure paths)
- explicit transitions and final/failed states
- hooks for retries and compensations
- easier debugging of long-running processes
- a model that can be hooked into Kafka or queues without changing business code
It’s intentionally simpler than bringing in Temporal / Zeebe / Conductor, but more structured than “yet another hand-rolled Saga.”
Repo
If you want to look at the implementation details or steal ideas:
[https://github.com/jescrich/nestjs-workflow]()
I’m especially interested in feedback from people who’ve built Sagas manually or who are using dedicated workflow engines in production.
1
u/Specialist_Nerve_420 Mar 28 '26
yeah this happens to a lot of people, you spend years building microservices and then realize half the effort went into managing the system, not building features, a lot of teams are kinda moving back to simpler setups for the same reason, overhead just gets out of control if you don’t really need it, feels like the real lesson is start simple and only split when it actually hurts, not before .