Decorators

Distributed System Decorators

Define TypeScript Saga workflows, compensation, transactional outbox and inbox processing, checkpoints, and resumable distributed operations with AxilJS.

6 min readDocumentationEdit this page

Distributed System Decorators

Distributed decorators describe workflows and transactional messaging patterns that span multiple services or execution boundaries.

AxilJS expresses these requirements as semantic metadata rather than coupling application code to a specific workflow engine, message broker, or distributed transaction implementation.

The core distributed decorators are:

  • @tSaga
  • @tStep
  • @tCompensate
  • @tOutbox
  • @tInbox
  • @tCheckpoint

@tSaga

Declares a class as a distributed Saga workflow.

A Saga models a sequence of operations that may execute across services. If a later step cannot complete, previously completed work can be compensated.

typescript
import {
  tSaga,
  tStep,
  tCompensate,
} from '@axiljs/decorator'
 
@tSaga('checkout')
class CheckoutWorkflow {
  @tStep('reserve-inventory')
  async reserveInventory() {}
 
  @tStep('charge-payment')
  async chargePayment() {}
 
  @tStep('create-order')
  async createOrder() {}
 
  @tCompensate('charge-payment')
  async refundPayment() {}
}

The Saga decorator identifies the workflow while its step and compensation metadata describe the individual execution units.

A Saga is particularly useful when a traditional database transaction cannot span the complete operation because the workflow crosses service or infrastructure boundaries.

@tStep

Declares an ordered step within a Saga or durable workflow.

typescript
import { tStep } from '@axiljs/decorator'
 
@tStep('reserve-inventory', {
  order: 1,
})
async reserve() {}
 
@tStep('charge-payment', {
  order: 2,
  optional: false,
})
async charge() {}

The step metadata defines the workflow structure without embedding orchestration logic directly into the method.

Step Options

OptionTypeDescription
ordernumberExecution order of the step
optionalbooleanIndicates whether the workflow can continue when the step is unsuccessful

The source defines ordered steps using order and supports explicit optionality through the optional property.

@tCompensate

Declares a compensating action for a previously completed workflow step.

typescript
import { tCompensate } from '@axiljs/decorator'
 
@tCompensate('charge-payment')
async refundPayment() {}

If a later workflow step fails, the workflow consumer can use the compensation metadata to determine which action should reverse or compensate the affected step.

Compensation is a central part of Saga-based distributed transactions because the original operation may no longer be reversible through a single database rollback.

@tOutbox

Declares transactional event publication using the Outbox pattern.

typescript
import {
  tTransaction,
  tOutbox,
} from '@axiljs/decorator'
 
@tTransaction()
@tOutbox()
async createOrder(dto: OrderDto) {
  const order = await this.orders.create(dto)
 
  return {
    order,
    events: [
      {
        type: 'order.created',
        payload: order,
      },
    ],
  }
}

The Outbox pattern coordinates event publication with the database transaction so that database state and the corresponding event are persisted consistently.

The source describes @tOutbox as providing at-least-once event delivery.

A typical implementation can persist the event in an outbox store as part of the transaction and publish it asynchronously afterward.

@tInbox

Declares processed-message tracking for incoming events.

typescript
import {
  tInbox,
  tOnEvent,
} from '@axiljs/decorator'
 
@tInbox({
  ttl: 86_400_000,
})
@tOnEvent('order.created')
async handleOrder(event: OrderEvent) {}

The Inbox pattern tracks processed message identifiers so duplicate deliveries can be detected.

The source describes @tInbox as providing at-most-once event consumption by tracking processed message IDs.

This is useful when message brokers provide redelivery or at-least-once delivery semantics.

@tCheckpoint

Marks a resumable point in a long-running workflow.

typescript
import { tCheckpoint } from '@axiljs/decorator'
 
@tCheckpoint('after-index')
async indexDocument() {}

If a workflow fails after reaching a checkpoint, a workflow consumer can resume from the checkpoint instead of restarting the entire workflow.

This is particularly useful for long-running processing pipelines where earlier steps are expensive or have already produced durable results.

Full Saga Example

The distributed decorators can be composed with other AxilJS semantics.

typescript
import {
  tSaga,
  tStep,
  tCompensate,
  tCheckpoint,
  tTimeout,
} from '@axiljs/decorator'
 
@tSaga('document-processing')
class DocumentWorkflow {
  @tStep('extract', {
    order: 1,
  })
  @tTimeout(30_000)
  async extract(documentId: string) {}
 
  @tStep('classify', {
    order: 2,
  })
  @tCheckpoint('after-classify')
  async classify(documentId: string) {}
 
  @tStep('index', {
    order: 3,
  })
  async index(documentId: string) {}
 
  @tCompensate('index')
  async removeFromIndex(documentId: string) {}
 
  @tCompensate('classify')
  async clearClassification(documentId: string) {}
}

This workflow describes:

  1. Extract the document.
  2. Classify the document.
  3. Persist the indexing result.
  4. Create a checkpoint after classification.
  5. Remove the index if the indexing step must be compensated.
  6. Clear classification if that step must be compensated.

The source uses this same pattern to demonstrate Saga steps, checkpoints, timeouts, and compensation working together.

Transactional Messaging

Distributed applications commonly need to coordinate database state and message publication.

Consider an order creation operation:

typescript
@tTransaction()
@tOutbox()
async createOrder(dto: CreateOrderDto) {
  const order = await this.orders.create(dto)
 
  return {
    order,
    events: [
      {
        type: 'order.created',
        payload: order,
      },
    ],
  }
}

The semantic declarations communicate two distinct requirements:

  • The database mutation belongs to a transaction.
  • The resulting event must participate in transactional outbox processing.

This allows the implementation to handle persistence and event delivery without forcing the business method to manage broker-specific mechanics.

Outbox and Inbox Together

For a distributed workflow, Outbox and Inbox semantics can be used on opposite sides of a message boundary.

Producer:

typescript
@tTransaction()
@tOutbox()
async createOrder(dto: OrderDto) {
  const order = await this.orders.create(dto)
 
  return {
    order,
    events: [
      {
        type: 'order.created',
        payload: order,
      },
    ],
  }
}

Consumer:

typescript
@tInbox({
  ttl: 86_400_000,
})
@tOnEvent('order.created')
async handleOrder(event: OrderEvent) {
  await this.fulfillment.create(event.payload)
}

The producer expresses reliable event publication while the consumer expresses duplicate-message handling.

Together, these semantics provide a declarative foundation for reliable asynchronous communication.

Workflow Resumption

Long-running workflows can combine steps and checkpoints:

typescript
@tSaga('video-processing')
class VideoWorkflow {
  @tStep('transcode', {
    order: 1,
  })
  async transcode(videoId: string) {}
 
  @tStep('analyze', {
    order: 2,
  })
  @tCheckpoint('after-analysis')
  async analyze(videoId: string) {}
 
  @tStep('publish', {
    order: 3,
  })
  async publish(videoId: string) {}
}

If the workflow fails during publication, a workflow consumer can use the checkpoint metadata to avoid repeating completed processing.

Distributed Decorator Reference

DecoratorPurpose
@tSagaDeclares a distributed Saga workflow
@tStepDefines an ordered workflow step
@tCompensateDefines compensation for a workflow step
@tOutboxDeclares transactional event publication
@tInboxTracks processed message identifiers
@tCheckpointDefines a resumable workflow point

These decorators form the distributed transaction category in AxilJS.

Design Principle

Distributed decorators should describe workflow intent, not implement the workflow engine.

For example:

typescript
@tSaga('checkout')
class CheckoutWorkflow {
  // ...
}

The decorator does not need to know whether the workflow is ultimately executed by a database-backed orchestrator, a state machine, or another workflow runtime.

The source explicitly describes the workflow engine as a separate consumer and gives Temporal, a custom state machine, and a database-driven orchestrator as possible implementations.

This separation keeps business code focused on domain operations while allowing infrastructure implementations to evolve independently.

Help improve the documentation

AxilJS is open source and documentation improvements are welcome.

AxilJS DocumentationMIT License · Built by SyntaxilitY