• 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 (5)

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

08.09.2022

Mobile application end-to-end test automation using Robot Framework

Teaching a robot to use a mobile phone: Learn how we write end-to-end tests for mobile applications.

25.08.2022

AURENA Tech Team Event 2022

The AURENA Tech Team Event 2022 brought together our team members from different European countries for two exciting days in Austria.

28.06.2022

Impressions of the AWS 2022 Berlin summit

Our DevOps Engineers report on their experience at the Amazon Web Services conference.

20.04.2022

Let's break the Vegas rule

Our Scrum Master GO shares her insights and main take-aways from the Atlassian Team ´22.

28.02.2022

AURENA Tech strengthens its DevOps team

We welcome Junior DevOps Engineer Danijel Maraz as the newest member to the AURENA Tech team.

Open positions

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

In this role, you will work on feature development and continuous improvement of our leading real-time auction platform.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 48,500 p.a.
AURENA.tech
text
Front-End Developer (HTML/CSS)

In this role, you will work on feature development and continuous improvement of our leading real-time auction platform.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 44,800 p.a.
AURENA.tech
text
Senior React Developer (f/m/x)

Build new high-performance apps from scratch and develop new features for our leading real-time auction platform

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 60,200 p.a.
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 € 60,200 p.a.
AURENA.tech
text
Senior Mobile App Developer (f/m/x)

Develop cross-platform native apps and progressive web apps from scratch.

  • Leoben or fully remote
  • Fulltime, permanent
  • Starts at € 60,200 p.a.