DEV Community

Cover image for A lightweight alternative to Temporal for node.js applications
Louis Dussarps
Louis Dussarps

Posted on

A lightweight alternative to Temporal for node.js applications

Temporal is a great tool for building resilient applications, but it can be challenging to host and operate. Today, we’re introducing a Node.js framework, orbiTS, that installs like any other npm package and lets you build crash-resilient apps in TypeScript—with zero infrastructure overhead.

Why explicitly orchestrate workflows?

Databases follow the principle of transactions — a set of changes that must either all succeed or all fail. But when an application interacts with multiple databases or connects to various APIs (as is the case for most applications today), the guarantees of ACID are lost. Workflows, state machines, and the saga pattern help achieve a similar level of reliability, often at the cost of more complex code.

Use case

In this post, we revisit a classic example: orchestrating a banking transaction.. This canonical example was provided by AWS Step Functions and Temporal. Readers can refer to these articles to compare the syntax and ease of implementation offered by each tool.

Problem

Let’s walk through a typical business logic: handling a stock trade.

Here are the typical steps:

  • Check the price of a stock
  • Generate a buy or sell recommendation
  • Execute the recommended action
  • On the surface, these are simple asynchronous calls that could be chained in a function:
async function trade() {
  const stockPrice = await checkPrice();
  const recommendation = await generateRecommendation(stockPrice);
  return recommendation === 'buy' ? await buyStock(stockPrice) : await sellStock(stockPrice);
}
Enter fullscreen mode Exit fullscreen mode

But in reality, problems accumulate:

  • What to do if a third-party service fails?
  • What if a network error occurs?
  • If the Node.js process is interrupted, the trade stops halfway, with no memory of the ongoing buy/sell operation.

These issues, far from being theoretical, can have financial consequences. For example, a buy or sell action that is forgotten or left halfway through can lead to losses for the company.

The Saga Orchestration Pattern

The Saga Orchestration Pattern effectively addresses these challenges. By centralizing workflow management in an orchestrator, this pattern mimics the transaction principle of a database. It allows a series of atomic actions, executed sequentially and under control, to be linked together into a global transaction.

The orchestrator:

  • Explicitly manages state transitions between each step.
  • Persists workflow state to ensure recovery after crashes.
  • Can replay actions in case of transient failure.
  • Provides detailed traceability through clear naming of each step. Thus, the Saga orchestration pattern not only guarantees resilience and consistency of operations but also facilitates maintenance, monitoring, and evolution of complex workflows in a distributed environment.

The Orbits implementation

Requirements

You can find the full example used in this blog post in this github folder
To try it out, follow this step-by-step guide on how to run and explore the workflow locally.
To install Orbits in your own project, refer to the install guide. It only takes a few lines to get started.

Workflow

Orbits proposes writing workflows in a structured and declarative manner.
Here is the concrete example :

export class TradingWorkflow extends Workflow{

 declare IResult:StockTransaction

 async define(){
  const checkPrice = await this.do("check-price", new CheckStockPriceAction());
  const stockPrice = checkPrice.stockPrice;

  const buyOrSell = await this.do("recommandation", 
    new GenerateBuySellRecommendationAction()
    .setArgument(
        {
            price:stockPrice.stock_price
        })
    ); 


  if (buyOrSell.buyOrSellRecommendation === 'sell') {
    const sell = await this.do("sell", new SellStockeAction().setArgument({
            price:stockPrice.stock_price
    }));
    return sell.stockData;
  } else {
    const buy = await this.do("buy", new BuyStockAction().setArgument({
            price:stockPrice.stock_price
    }));
    return buy.stockData;
  }
 };
}
Enter fullscreen mode Exit fullscreen mode

This central workflow orchestrates each step by calling autonomous Actions, while maintaining branching logic and intermediate states.

  • Explicit orchestration: The Orbits engine handles workflow state persistence, ensuring the process can recover and resume from the exact step it failed—no matter when or why the crash occurred
  • Atomic actions: Each business step is an independent and testable action
  • Conditional branching: The workflow flow can diverge based on data (buy or sell). It does not differ from standard TypeScript code.
  • Extensibility: We can easily add steps, compensation logic, monitoring
  • Resilience: Handles crash recovery, workflow state, and observability Each step is defined as an Orbits Action.

Action

export class BuyStockAction extends Action {
    async main() {
        const response = await fetch(API_ADDRESS + 'buyStock', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ stock_price: this.argument.price })
        });
        this.result.stockData = await response.json();
        return ActionState.SUCCESS;
    }
}
Enter fullscreen mode Exit fullscreen mode

This action:

  • Takes a typed input (price)
  • Calls a remote API in an encapsulated manner
  • Returns a state - ActionState.SUCCESS, ready to be recorded and resumed
  • Handles errors by default via a state - ActionState.ERROR

This structuring makes the action not only easy to test in isolation but also reusable in different workflows, while simplifying its instrumentation for monitoring or debugging.

Benefit summary

Adopting Orbits offers:

Standard TypeScript

Orbits is a standard TypeScript framework. You write promises and asynchronous functions just like you would anywhere else.

Clear separation of responsibilities

  • Workflow = orchestration
  • Action = unit business logic

Flexibility & Scalability

  • We can modify the flow without touching business components
  • Actions are reusable in multiple workflows

Resilience and recovery

  • Orbits manages state persistence
  • Automatic recovery from the last valid point

Native observability

  • Each action is traceable, named, monitorable

Going further

Using lambda async invocations

For the sake of hypothesis, let’s assume our Lambda function runs for an extended period of time (which is not the case here). In such scenarios, there’s a high chance that the initial HTTP call triggering the Lambda might fail unexpectedly—due to a network issue or timeout, for example.

To prevent such failures from disrupting the overall workflow, you can configure retry policies.

Orbits also supports asynchronous APIs and allows you to track execution status over time. When dealing with long-running Lambda functions, an Orbits action can return with an ActionState.IN_PROGRESS, and then delegate the follow-up logic to the watcher() method, which periodically checks the progress of the async process.

This approach requires a bit of additional setup, as you’ll need to interact with the AWS Lambda API to track the specific invocation’s result.

We’ll cover how Orbits makes it easy to manage long-running processes in a dedicated blog post soon.

Using resources to manage concurrency

In our example, if we trigger the same order twice, it will be processed twice—this isn’t always the desired behavior. Orbits provides an opinionated way to handle concurrency through a concept called resources. Resources allow you to control and limit the execution of actions to prevent unintended duplication.

Conclusion

With the simplicity of Orbits in Node.js, we can build systems that are reliable, readable, and maintainable, without changing your coding practices.

For your critical processes — e-commerce, finance, logistics, etc. — adopting such an approach will significantly reduce your bug rate and inconsistencies.


Learn more about Orbits and its capabilities in our documentation

Top comments (2)

Collapse
 
camille-leroy profile image
Camille Leroy

Nice post! Has anyone tried this in a production environment?

Collapse
 
louis_dussarps_e656bc7b01 profile image
Louis Dussarps

Yes, we've been using it in production for at least 3 years and now have published it openSource.