AWS Developer Blog

AWS Step Functions Fluent Java API

by Andrew Shore | on | in Java | Permalink | Comments |  Share

AWS Step Functions, a new service that launched at re:Invent 2016, makes it easier to build complex, distributed applications in the cloud. Using this service, you can create state machines that can connect microservices and activities into a visual workflow. State machines support branching, parallel execution, retry/error handling, synchronization (via Wait states), and task execution (via AWS Lambda or an AWS Step Functions Activity).

The Step Functions console provides excellent support for visualizing and debugging a workflow and for creating state machine descriptions. State machines are described in a JSON document, as described in detail here. Although the console has a great editor for building these documents visually, you might want to write state machines in your IDE via a native Java API. Today, we’re launching a fluent builder API to create state machines in a readable, compact way. This new API is included in the AWS SDK for Java.

 

To get started, create a new Maven project and declare a dependency on the aws-java-sdk-stepfunctions client.

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-stepfunctions</artifactId>
    <version>1.11.86</version>
</dependency>

Let’s take a look at some examples. We’ll go through each blueprint available in the console and translate that to the Java API.

Hello World

The following is a JSON representation of a simple state machine that consists of a single task state. The task calls out to a Lambda function (identified by ARN), passing the input of the state machine to the function. When the function completes successfully, the state machine terminates with the same output as the function.
JSON

{
  "Comment" : "A Hello World example of the Amazon States Language using an AWS Lambda Function",
  "StartAt" : "Hello World",
  "States" : {
    "Hello World" : {
      "End" : true,
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Type" : "Task"
    }
  }
}

Java API
Let’s rewrite this simple state machine using the new Java API and transform it to JSON. Be sure you include the static import for the fluent API methods.


package com.example;

import static com.amazonaws.services.stepfunctions.builder.StepFunctionBuilder.*;
import com.amazonaws.services.stepfunctions.builder.ErrorCodes;

public class StepFunctionsDemo {

    public static void main(String[] args) {
        final StateMachine stateMachine = stateMachine()
                .comment("A Hello World example of the Amazon States Language using an AWS Lambda Function")
                .startAt("Hello World")
                .state("Hello World", taskState()
                        .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                        .transition(end()))
                .build();
        System.out.println(stateMachine.toPrettyJson());
    }
}

Let’s take a closer look at the previous example. The very first method you will always call when constructing a state machine, is stateMachine(). This returns a mutable StateMachine.Builder that can be used to configure all properties of a state machine. Here, we’re adding a comment describing the purpose of the state machine, indicating the initial state via the startAt() method, and defining that state via the state() method. Each state machine must have at least one state in it and must have a valid path to a terminal state (that is, a state that causes the state machine to end). In this example, we have a single TaskState (configured via the taskState() method) that also serves as the terminal state via the End transition (configured by transition(end()) ).

Once you configure a state machine to your liking, you can call the build() method on the StateMachineBuilder to produce an immutable StateMachine object. This object can then be transformed into JSON (see toJson() and toPrettyJson()) or it can be passed directly to the CreateStateMachine API in the Java SDK (see below).

The following creates the state machine (created previously) via the service client. The definition() method can take either the raw JSON or a StateMachine object. For more information about getting started with the Java SDK, see our AWS Java Developer Guide.

final AWSStepFunctions client = AWSStepFunctionsClientBuilder.defaultClient();
client.createStateMachine(new CreateStateMachineRequest()
                                          .withName("Hello World State Machine")
                                          .withRoleArn("arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME")
                                          .withDefinition(stateMachine));

 

Wait State

The following state machine demonstrates various uses of the Wait state type, which can be used to wait for a given amount of time or until a specific time. Wait states can dynamically wait based on input using the TimestampPath and SecondsPath properties, which are JSON reference paths to a timestamp or an integer, respectively. The Next property identifies the state to transition to after the wait is complete.
JSON

{
  "Comment" : "An example of the Amazon States Language using wait states",
  "StartAt" : "First State",
  "States" : {
    "First State" : {
      "Next" : "Wait Using Seconds",
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Type" : "Task"
    },
    "Wait Using Seconds" : {
      "Seconds" : 10,
      "Next" : "Wait Using Timestamp",
      "Type" : "Wait"
    },
    "Wait Using Timestamp" : {
      "Timestamp" : "2017-01-16T19:18:55.103Z",
      "Next" : "Wait Using Timestamp Path",
      "Type" : "Wait"
    },
    "Wait Using Timestamp Path" : {
      "TimestampPath" : "$.expirydate",
      "Next" : "Wait Using Seconds Path",
      "Type" : "Wait"
    },
    "Wait Using Seconds Path" : {
      "SecondsPath" : "$.expiryseconds",
      "Next" : "Final State",
      "Type" : "Wait"
    },
    "Final State" : {
      "End" : true,
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Type" : "Task"
    }
  }
}

Java API
Again, we call the stateMachine() method to begin constructing the state machine. Our start-at state is a Task state that has a transition to the Wait Using Seconds state. The Wait Using Seconds state is configured to wait for 10 seconds before proceeding to the Wait Using Timestamp state. Notice that we use the waitState() method to obtain an instance of WaitState.Builder, which we then use to configure the state. The waitFor() method can accept different types of wait strategies (Seconds, SecondsPath, Timestamp, TimestampPath). Each strategy has a corresponding method in the fluent API (seconds, secondsPath, timestamp, and timestampPath, respectively). Both the SecondsPath and TimestampPath strategies require a valid JsonPath that references data in the input to the state. This input is then used to determine how long to wait for.

final Date waitUsingTimestamp =
        Date.from(LocalDateTime.now(ZoneOffset.UTC).plusMinutes(15).toInstant(ZoneOffset.UTC));
final StateMachine stateMachine = stateMachine()
        .comment("An example of the Amazon States Language using wait states")
        .startAt("First State")
        .state("First State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(next("Wait Using Seconds")))
        .state("Wait Using Seconds", waitState()
                .waitFor(seconds(10))
                .transition(next("Wait Using Timestamp")))
        .state("Wait Using Timestamp", waitState()
                .waitFor(timestamp(waitUsingTimestamp))
                .transition(next("Wait Using Timestamp Path")))
        .state("Wait Using Timestamp Path", waitState()
                .waitFor(timestampPath("$.expirydate"))
                .transition(next("Wait Using Seconds Path")))
        .state("Wait Using Seconds Path", waitState()
                .waitFor(secondsPath("$.expiryseconds"))
                .transition(next("Final State")))
        .state("Final State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(end()))
        .build();
System.out.println(stateMachine.toPrettyJson());

Retry Failure

Retriers are a mechanism to retry certain types of states on a given set of error codes. They define both the condition on which to retry (via ErrorEquals) and the backoff behavior and maximum number of retry attempts. At the time of this post, they may be used only with Task states and Parallel states. In the following state machine, the Task state has three retriers. The first retrier retries a custom error code named HandledError that might be thrown from the Lambda function. The initial delay of the first retry attempt is one second (as defined by IntervalSeconds). The maximum number of retry attempts is set at five. The BackoffRate is used for subsequent retries to determine the next delay; for example, the delays for the first retrier would be 1, 2, 4, 8, etc. The second retrier uses a predefined error code available that is matched whenever the task fails (for whatever reason). A full list of predefined error codes can be found here. Finally, the last retrier uses the special error code States.ALL to retry on everything else. If you use the States.ALL error code, it must appear in the last retrier and must be the only code present in ErrorEquals.
JSON

{
  "Comment" : "A Retry example of the Amazon States Language using an AWS Lambda Function",
  "StartAt" : "Hello World",
  "States" : {
    "Hello World" : {
      "End" : true,
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Retry" : [ {
        "ErrorEquals" : [ "HandledError" ],
        "IntervalSeconds" : 1,
        "MaxAttempts" : 5,
        "BackoffRate" : 2.0
      }, {
        "ErrorEquals" : [ "States.TaskFailed" ],
        "IntervalSeconds" : 30,
        "MaxAttempts" : 2,
        "BackoffRate" : 2.0
      }, {
        "ErrorEquals" : [ "States.ALL" ],
        "IntervalSeconds" : 5,
        "MaxAttempts" : 5,
        "BackoffRate" : 2.0
      } ],
      "Type" : "Task"
    }
  }
}

Java API

Let’s see what the previous example looks like in the Java API. Here we use the retrier() method to configure a Retrier.Builder. The errorEquals() method can take one or more error codes that indicate what this retrier handles. The second retrier uses a constant defined in the ErrorCodes class, which contains all predefined error codes supported by the States language. The last retrier uses a special method, retryOnAllErrors(), to indicate the retrier handles any other errors. This is equivalent to errorEquals("States.ALL") but is easier to read and easier to remember. Again, the “retry all” retrier must be last or a validation exception will be thrown.

final StateMachine stateMachine = stateMachine()
        .comment("A Retry example of the Amazon States Language using an AWS Lambda Function")
        .startAt("Hello World")
        .state("Hello World", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(end())
                .retrier(retrier()
                                 .errorEquals("HandledError")
                                 .intervalSeconds(1)
                                 .maxAttempts(5)
                                 .backoffRate(2.0))
                .retrier(retrier()
                                 .errorEquals(ErrorCodes.TASK_FAILED)
                                 .intervalSeconds(30)
                                 .maxAttempts(2)
                                 .backoffRate(2.0))
                .retrier(retrier()
                                 .retryOnAllErrors()
                                 .intervalSeconds(5)
                                 .maxAttempts(5)
                                 .backoffRate(2.0))
        )
        .build();

System.out.println(stateMachine.toPrettyJson());

Catch Failure

Catchers are a similar error handling mechanism. Like Retriers, they can be defined to handle certain error codes that can be thrown from a state. Catchers define a state transition that occurs when the error code matches the ErrorEquals list. The transition state can handle the recovery steps needed for that particular failure scenario. Much like retriers, ErrorEquals can contain one or more error codes (either custom or predefined). The States.ALL is a special catch all that must be in the last Catcher, if present.
JSON

{
  "Comment" : "A Catch example of the Amazon States Language using an AWS Lambda Function",
  "StartAt" : "Hello World",
  "States" : {
    "Hello World" : {
      "End" : true,
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Catch" : [ {
        "Next" : "Custom Error Fallback",
        "ErrorEquals" : [ "HandledError" ]
      }, {
        "Next" : "Reserved Type Fallback",
        "ErrorEquals" : [ "States.TaskFailed" ]
      }, {
        "Next" : "Catch All Fallback",
        "ErrorEquals" : [ "States.ALL" ]
      } ],
      "Type" : "Task"
    },
    "Custom Error Fallback" : {
      "End" : true,
      "Result" : "This is a fallback from a custom lambda function exception",
      "Type" : "Pass"
    },
    "Reserved Type Fallback" : {
      "End" : true,
      "Result" : "This is a fallback from a reserved error code",
      "Type" : "Pass"
    },
    "Catch All Fallback" : {
      "End" : true,
      "Result" : "This is a fallback from a reserved error code",
      "Type" : "Pass"
    }
  }
}

Java API

To configure a catcher, first call the catcher() method to obtain a Catcher.Builder. The first Catcher handles the custom error code HandledError, and transitions to the Custom Error Fallback state. The second handles the predefined States.TaskFailed error code, and transitions to the Reserved Type Fallback state. Finally, the last catcher handles all remaining errors and transitions to the Catch All Fallback state. Like Retriers, there is a special method, catchAll(), that configures the catcher to handle all error codes. Use of catchAll() is preferred over errorEquals("States.ALL").

final StateMachine stateMachine = stateMachine()
        .comment("A Catch example of the Amazon States Language using an AWS Lambda Function")
        .startAt("Hello World")
        .state("Hello World", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(end())
                .catcher(catcher()
                                 .errorEquals("HandledError")
                                 .transition(next("Custom Error Fallback")))
                .catcher(catcher()
                                 .errorEquals(ErrorCodes.TASK_FAILED)
                                 .transition(next("Reserved Type Fallback")))
                .catcher(catcher()
                                 .catchAll()
                                 .transition(next("Catch All Fallback"))))
        .state("Custom Error Fallback", passState()
                .result("\"This is a fallback from a custom lambda function exception\"")
                .transition(end()))
        .state("Reserved Type Fallback", passState()
                .result("\"This is a fallback from a reserved error code\"")
                .transition(end()))
        .state("Catch All Fallback", passState()
                .result("\"This is a fallback from a reserved error code\"")
                .transition(end()))
        .build();

System.out.println(stateMachine.toPrettyJson());

Parallel State

You can use a Parallel state to concurrently execute multiple branches. Branches are themselves pseudo state machines and can contain multiple states (and even nested Parallel states). The Parallel state waits until all branches have terminated successfully before transitioning to the next state. Parallel states support retriers and catchers in the event that execution of a branch fails.
JSON

{
  "Comment": "An example of the Amazon States Language using a parallel state to execute two branches at the same time.",
  "StartAt": "Parallel",
  "States": {
    "Parallel": {
      "Type": "Parallel",
      "Next": "Final State",
      "Branches": [
        {
          "StartAt": "Wait 20s",
          "States": {
            "Wait 20s": {
              "Type": "Wait",
              "Seconds": 20,
              "End": true
            }
          }
        },
        {
          "StartAt": "Pass",
          "States": {
            "Pass": {
              "Type": "Pass",
              "Next": "Wait 10s"
            },
            "Wait 10s": {
              "Type": "Wait",
              "Seconds": 10,
              "End": true
            }
          }
        }
      ]
    },
    "Final State": {
      "Type": "Pass",
      "End": true
    }
  }
}

Java API

To create a Parallel state in the Java API, call the parallelState() method to obtain an instance of ParallelState.Builder. Next, you can add branches of execution via the branch() method. Each branch must have StartAt (name of initial state for branch) specified and at least one state.

final StateMachine stateMachine = stateMachine()
        .comment(
                "An example of the Amazon States Language using a parallel state to execute two branches at the same time.")
        .startAt("Parallel")
        .state("Parallel", parallelState()
                .transition(next("Final State"))
                .branch(branch()
                                .startAt("Wait 20s")
                                .state("Wait 20s", waitState()
                                        .waitFor(seconds(20))
                                        .transition(end())))
                .branch(branch()
                                .startAt("Pass")
                                .state("Pass", passState()
                                        .transition(next("Wait 10s")))
                                .state("Wait 10s", waitState()
                                        .waitFor(seconds(10))
                                        .transition(end()))))
        .state("Final State", passState()
                .transition(end()))
        .build();

System.out.println(stateMachine.toPrettyJson());
System.out.println(stateMachine.toPrettyJson());

Choice State

A Choice state adds branching logic to a state machine. It consists of one or more choices and, optionally, a default state transition if no choices matches. Each choice rule represents a condition and a transition to enact if that condition evaluates to true. Choice conditions can be simple (StringEquals, NumericLessThan, etc) or composite conditions using And, Or, Not.

In the following example, we have a choice state with two choices, both using the NumericEquals condition, and a default transition if neither choice rule matches.
JSON

{
  "Comment" : "An example of the Amazon States Language using a choice state.",
  "StartAt" : "First State",
  "States" : {
    "First State" : {
      "Next" : "Choice State",
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Type" : "Task"
    },
    "Choice State" : {
      "Default" : "Default State",
      "Choices" : [ {
        "Variable" : "$.foo",
        "NumericEquals" : 1,
        "Next" : "First Match State"
      }, {
        "Variable" : "$.foo",
        "NumericEquals" : 2,
        "Next" : "Second Match State"
      } ],
      "Type" : "Choice"
    },
    "First Match State" : {
      "Next" : "Next State",
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:OnFirstMatch",
      "Type" : "Task"
    },
    "Second Match State" : {
      "Next" : "Next State",
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:OnSecondMatch",
      "Type" : "Task"
    },
    "Default State" : {
      "Cause" : "No Matches!",
      "Type" : "Fail"
    },
    "Next State" : {
      "End" : true,
      "Resource" : "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Type" : "Task"
    }
  }
}

Java API

To add a Choice state to your state machine, use the choiceState() method to obtain an instance of ChoiceState.Builder. You can add choice rules via the choice() method on the builder. For simple conditions, there are several overloads for each comparison operator (LTE, LT, EQ, GT, GTE) and data types (String, Numeric, Timestamp, Boolean). In this example, we’re using the eq() method that takes a string as the first argument, which is the JsonPath expression referencing the input data to apply the condition to. The second argument will differ depending on the type of data you are comparing against. Here we’re using an integer for numeric comparison. Each choice rule must have a transition that should occur if the condition evaluates to true.

final StateMachine stateMachine = stateMachine()
        .comment("An example of the Amazon States Language using a choice state.")
        .startAt("First State")
        .state("First State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(next("Choice State")))
        .state("Choice State", choiceState()
                .choice(choice()
                                .transition(next("First Match State"))
                                .condition(eq("$.foo", 1)))
                .choice(choice()
                                .transition(next("Second Match State"))
                                .condition(eq("$.foo", 2)))
                .defaultStateName("Default State"))
        .state("First Match State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:OnFirstMatch")
                .transition(next("Next State")))
        .state("Second Match State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:OnSecondMatch")
                .transition(next("Next State")))
        .state("Default State", failState()
                .cause("No Matches!"))
        .state("Next State", taskState()
                .resource("arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME")
                .transition(end()))
        .build();

System.out.println(stateMachine.toPrettyJson());

You can find more references and tools for building state machines in the Step Functions documentation, and post your questions and feedback to the Step Functions Developers Forum.