• bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
  • bids: 0 +10
  • bidders: 0 +10
  • completed auctions: 0 +10
  • next fall of the hammer: 0 ending now
  • sold lots: 0 +10
Job openings (3)

17.10.2022

JavaScript State Machine for Microservice Orchestration with XState

We have started using XState to build our saga orchestration, and it’s made it so much easy to understand what is going on with service coordination.

written by Holly Smith

The backend team of AURENA Tech is building a new microservice for our customer service staff. The backend is complicated because several services coordinate with each other.

We had a section of work that would be ideal for experimenting with a state machine implementation. It is a pattern we could then take to build larger, more complicated systems in the future. So I was tasked with implementing the state machine inside a micro-service to handle the complaint management workflow for lot distribution.

Requirements

A Lot Distribution Complaint (LDC) is a record of a refusal to accept a specific distributed lot by the person collecting the lot during a distribution day. The design specification for the workflow of an individual complaint record was written as a state machine (using a MermaidJS plugin in our Confluence Cloud), with the state transitions clearly defined and state entry behaviour specified:

Workflow of a complaint record

We decided to use an actual state machine implementation in the codebase to explicitly model these state transitions and states as specified.

XStateJS

We evaluated several existing JavaScript state machine libraries and alternatively considered writing our own simple state machine engine. Our selection criteria included:

  • Feature set: We have far more complex workflows to design and implement in the near future, so we needed to find an engine that would allow us to express more complex logic in the state machine engine directly.
  • Stateless: Ability to save and reload the state machine “state” before and after every state transition. The event-based microservice architecture framework that we use, called Eventicle, load-balances event processing across multiple runtime instances. The state transitions for a single Complaint record may occur across different instances at different times, so the engine must allow its shared state to be loaded from, and saved to, a common persistence datastore every time.
  • TypeScript-friendly: All our JavaScript codebase is written in TypeScript, so any library had to have first-class TypeScript support.
  • Excellent development experience: Comprehensive documentation, an actively maintained library and unit testing support.

Using the above criteria we evaluated that XStateJS fulfilled our requirements. Because the Complaints workflow is just a simple finite state machine with no nested or parallel state, could have just done an implementation using a switch statement. However, we also wanted to use this as a proof of concept so we can handle more complicated workflows in the near future.

More on this: Introduction to state machines and statecharts

XStateJS Statechart implementation

The JSON below is our implementation of the Complaints statechart in XStateJS.

The states object lists the states and state transitions, taken directly from the design specification. We love how this code maps directly to the specification. It is immediately obvious if this is complete or if we forgot to include a state transition in the implementation. If we implemented this by a series of unconnected, tangled methods, we would have to repeatedly follow the convoluted code path manually to check this.

As we use TypeScript in our projects, we strongly typed the state machine. To do this we used the XState Typegen library as it automatically generates intelligent typings for XState. This means that the events in the options are “strongly typed to the events that cause the action to be triggered.” (Source: XState – JavaScript State Machines and Statecharts)

                createMachine(
    {
      id: "lot-distribution-complaint",
      strict: true,
      initial: "idle",
      tsTypes: {} as import("./complaint-state-machine.typegen").Typegen0,
      schema: {
        context: {} as LDCContext,
        events: {} as LDCEvent,
      },
      context: {
        aggregate: agg,
      },
      states: {
        idle: {
          on: {
            LDC_CREATED_BY_BO: { target: "open", actions: ["ldc.created"] },
            LDC_CREATED_BY_DW: { target: "open", actions: ["ldc.created"] },
          },
        },
        open: {
          on: {
            LDC_RESOLVED_BY_BO: { target: "resolved", actions: ["ldc.resolved"] },
            LDC_COMMENTED_BY_BO: { target: "open", actions: ["ldc.commented"] },
            LDC_DELETED_BY_DW: { target: "deleted", actions: ["ldc.deleted"] },
            LDC_REPLACED_BY_DW: { target: "replaced", actions: ["ldc.replaced"] },
          },
        },
        deleted: {
          type: "final",
        },
        resolved: {
          on: {
            LDC_REOPENED_BY_BO: { target: "open", actions: ["ldc.reopened"] },
          },
        },
        replaced: {
          type: "final",
        },
      },
    },
  )
            

Diagram, generated by using the XState Visualizer; Source: XState Visualizer

The above diagram shows a visual representation of the state machine JSON. It was generated using the XState Visualizer. It is similar to the diagram we use for our requirements and specifications, again making it easy to spot if we have got it right or not early on in the process.

XState state actions

The actual business logic for each state transition is encapsulated in a series of pure XState Action functions, which are easy to mock and unit test individually.

Mostly, the Complaint action functions simply build and emit an Eventicle Event to be emitted (via Kafka). This enables other Eventicle components to consume these events and perform further work.

One gotcha we hit was the realisation that XState Action functions are synchronous; they cannot block on promise-returning method calls (e.g. using “await” ). Sometimes we needed to call async methods to enrich the XState event data (e.g. from additional data in the database). The canonical XState way of handling promises in action functions is to implement the promise as a nested series of pending, fulfilled and rejected substates. We felt this would quickly become too complex especially if there were two or more sequential async calls needed in one action. After some experimentation, we decided to ensure the XState event included all the data needed to raise the emitted Eventicle Events, removing the need to do the enrichment work in the action functions themselves. We may revisit this in the future.

Action example – Add a comment to an OPEN LDC

                export function ldcCommentedAction(
  context: LDCContext,
  event: XStateCommentedByBOEvent,
  actionMeta: ActionMeta
) {
  let data: ProjectLDCCommentDataV1 = {
    commentNote: event.commentNote,
    commentedByBOUserId: event.commentedByBOUserId,
    complaintState: getComplaintStateFromXStateState(actionMeta.state),
  };
  context.aggregate.raiseEvent({
    type: LotDistributionComplaintEventTypes.COMMENTED,
    data,
  } as ProjectLDCCommentV1);
}
            

Upon receiving an LDC_COMMENTED_BY_BO state, the state machine triggers a ldcCommentedAction function. The XState event data is then converted to an Eventicle event, which is then emitted.

Unit testing

                it("can comment on an OPEN complaint", async () => {
    //SETUP
    const ldcId = await LotDistributionComplaintCommand.CREATE_BY_DW(
      "btdaId",
      "btLotId",
      "complaintCode",
      false,
      Date.now(),
      null,
      null,
      "i-am-dw"
    );
    
    // EXECUTE
    await LotDistributionComplaintCommand.COMMENT_BY_BO(ldcId, "comment note", "9977");
    
    // VERIFY
    const events = await consumeFullEventLog(EventStream.LDC);

    expect(events.map((value) => value.type)).toStrictEqual([
      LotDistributionComplaintEventTypes.CREATED,
      LotDistributionComplaintEventTypes.COMMENTED,
    ]);

    expect(events[0].data).toStrictEqual(createCommandMock);
    expect(events[1].data).toStrictEqual(commentCommandMock);
  });
            

The above example of a unit test for the state machine tests that a comment can be added to the complaint. A Lot Distribution Complaint (LDC) is created in the setup, by emitting the CREATE_BY_DW Eventicle command. Once that has finished emitting, then the COMMENT_BY_BO Eventicle command is executed. Both Eventicle events are received by the state machine and processed. To verify this, the Eventicle event log is checked to see that the expected events were published. The data of the events are also checked.

In conclusion, we successfully implemented our system’s first state machine integrated with Eventicle. We will be taking what we have learnt from this to build larger and more complicated systems.

Interested in working with AURENA Tech? Take a look at the open positions.

Article by
Holly Smith

Holly Smith is a Full Stack Engineer and supports the Aurena TECH Team as an external consultant.

More articles

04.03.2024

AURENA Tech Winter Games 2024

Having successfully found snow and ice, we spent a fun day together in the Styrian mountains.

08.01.2024

AURENA Tech welcomes new team members

We are happy to welcome two new colleagues to our engineering team: Frontend Lead Patrick Niebrzydowski and Senior Frontend Engineer Dmytro Barylo.

28.12.2023

AURENA celebrates milestone with 200,000 bidders

Andrea Brezina was the lucky one to register as our 200,000th bidder - AURENA welcomes her with a gift voucher just before Christmas.

20.10.2023

Meet our Backend Lead Christian Prohinig

After working for companies like Bitpanda and Dynatrace, Christian Prohinig now joins the engineering team of AURENA Tech as Backend Lead.

22.08.2023

AURENA Tech Team Event 2023

Amazing weather, amazing locations, and – most importantly – amazing people: Two days of culinary delights, adventure, and relaxation in Austria.

Open positions

AURENA.tech
text
Senior Node.js Developer (f/m/x)

This role offers you the opportunity to lead middleware and microservice development at AURENA Tech.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 69,300 p.a.
AURENA.tech
text
CI/CD Automation Engineer (f/m/x)

In this role you will design and maintain CI/CD pipelines, manage Docker containers and support the team with test automation.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 51,800 p.a.
AURENA.tech
text
Senior Cloud Engineer AWS (f/m/x)

In this role you will design, develop and maintain AWS infrastructure with Terraform and manage our Kubernetes clusters.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 69,300 p.a.