Build a Temporal "Hello World!" app from scratch

You're taking the next step in your journey towards building better apps!

This tutorial is for developers who are relatively new to Temporal and have some basic knowledge of Java. We recommend setting aside ~20 minutes to complete. By following this tutorial you will achieve a few things:

  • Learn how to set up a Temporal Java application project.
  • Become more familiar with core concepts and the application structure.
  • Build and test a simple "Hello World!" Temporal Workflow application from scratch using the Temporal Java SDK and Gradle.

This tutorial focuses on the practicalities of building an application from scratch. To better understand why you should use Temporal, we recommend that you follow the tutorial where you run a Temporal money transfer application to get a taste of its value propositions.

All of the code in this tutorial is available in the hello-world Java template repository.

   Scaffold Gradle

Before starting, make sure you have looked over the tutorial prerequisites.

Create a new project directory called "hello-world-tutorial", or something similar.

You can scaffold a new Gradle project from the terminal or from within IntelliJ.

Terminal:

Change your working directory to the one created for the project and follow Gradle's Building Java Applications guide. When you get to the step where you define your source package, use "helloworldapp".

IntelliJ

Open IntelliJ and create a new Gradle project by following Step 1 of the Getting started with Gradle guide. When you get to the step where you name the project, use "helloworldapp" and make sure you choose the "hello-world-tutorial" directory as the project location. It will take a few moments to complete.

   Project dependencies

Once Gradle has finished scaffolding you will need to customize the project dependencies. To do this, open the build.gradle file that is in the root of your project and add the following lines to the dependencies section. If you want to try using different versions of dependencies, you can find them on search.maven.org (Temporal SDK versions):

build.gradle

dependencies {
// Application dependencies
implementation 'com.google.guava:guava:29.0-jre'
implementation 'io.temporal:temporal-sdk:1.0.0'
implementation 'ch.qos.logback:logback-classic:1.2.3'
// Testing dependencies
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-all:1.10.19'
}
  • com.google.guava:guava offers a suit of core and expanded libraries that Gradle uses.
  • io.temporal:temporal-sdk enables communication with the Temporal server.
  • ch.qos.logback:logback-classic will ensure that there is a logger to bind to within the SDK and prevent a default logger warning message.

A "refresh" icon will appear on the screen, click it to load the changes. Gradle will rebuild with the dependencies.

To limit the logging output from the SDK, within src/main/resources/ create a logback.xml file and paste in the following XML:

src/main/resources/logback.xml

<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.grpc.netty" level="INFO" />
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

   "Hello World!" app

Now we are ready to build our Temporal Workflow application. Our app will consist of four pieces:

  1. An Activity: An Activity is just a function, in this case an object method, that contains your business logic. Ours will simply format some text and return it.
  2. A Workflow: Workflows are functions that organize Activity method calls. Our Workflow will have a single entry method which calls the Activity object method.
  3. A Worker: Workers host the Activity and Workflow code and execute the code piece by piece.
  4. An initiator: To start a Workflow, we must send a signal to the Temporal server that tells it to track the state of the Workflow. We will write a separate function to do this.

All of the files for our application will be created in src/main/java/helloworldapp/. Gradle will have generated a default App.java class in that location. Remove it before proceeding.

Activity

First, let's define our Activity object and its method. Activities are meant to handle non-deterministic code that could result in unexpected results or errors. But for this tutorial all we are doing is taking a string, appending it to "Hello", and returning it back to the Workflow.

An Activity object is defined like any other object in Java. You need an interface and an implementation. The only difference is that the interface includes Temporal decorators. Let's create a Format object with a composeGreeting() method.

Create Format.java and add the following interface definition:

src/main/java/helloworldapp/Format.java

package helloworldapp;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
@ActivityInterface
public interface Format {
@ActivityMethod
String composeGreeting(String name);
}

Create FormatImpl.java and define the implementation of the Format interface:

src/main/java/helloworldapp/FormatImpl.java

package helloworldapp;
public class FormatImpl implements Format {
@Override
public String composeGreeting(String name) {
return "Hello " + name + "!";
}
}

Workflow

Next is our Workflow. Workflow functions are where you configure and organize the execution of Activity functions. Again, the Workflow object is defined like any other, except the interface includes Temporal decorators. Our Workflow has just a single entry method which calls the composeGreeting() Activity method and returns the result.

Create HelloWorldWorkflow.java and define the Workflow interface:

src/main/java/helloworldapp/HelloWorldWorkflow.java

package helloworldapp;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface HelloWorldWorkflow {
@WorkflowMethod
String getGreeting(String name);
}

Create HelloWorldWorkflowImpl.java and define the Workflow:

src/main/java/helloworldapp/HelloWorldWorkflowImpl.java

package helloworldapp;
import io.temporal.activity.ActivityOptions;
import io.temporal.workflow.Workflow;
import java.time.Duration;
public class HelloWorldWorkflowImpl implements HelloWorldWorkflow {
ActivityOptions options = ActivityOptions.newBuilder()
.setScheduleToCloseTimeout(Duration.ofSeconds(2))
.build();
// ActivityStubs enable calls to Activities as if they are local methods, but actually perform an RPC.
private final Format format = Workflow.newActivityStub(Format.class, options);
@Override
public String getGreeting(String name) {
// This is the entry point to the Workflow.
// If there were other Activity methods they would be orchestrated here or from within other Activities.
return format.composeGreeting(name);
}
}

Task Queue

Task Queues are how the Temporal server supplies information to Workers. When you start a Workflow, you tell the server which Task Queue the Workflow and/or Activities use as an information queue. We will configure our Worker to listen to the same Task Queue that our Workflow and Activities use. Since the Task Queue name is used by multiple things, let's create Shared.java and define our Task Queue name there:

src/main/java/helloworldapp/Shared.java

package helloworldapp;
public interface Shared {
String HELLO_WORLD_TASK_QUEUE = "HELLO_WORLD_TASK_QUEUE";
}

Worker

Our Worker hosts Workflow and Activity functions and executes them one at a time. The Worker is instructed to execute the specific functions via information it gets from the Task Queue, and after execution, it communicates results back to the server.

Create HelloWorldWorker.java and define the Worker:

src/main/java/helloworldapp/HelloWorldWorker.java

package helloworldapp;
import io.temporal.client.WorkflowClient;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
public class HelloWorldWorker {
public static void main(String[] args) {
// This gRPC stubs wrapper talks to the local docker instance of the Temporal service.
WorkflowServiceStubs service = WorkflowServiceStubs.newInstance();
WorkflowClient client = WorkflowClient.newInstance(service);
// Create a Worker factory that can be used to create Workers that poll specific Task Queues.
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker(Shared.HELLO_WORLD_TASK_QUEUE);
// This Worker hosts both Workflow and Activity implementations.
// Workflows are stateful, so you need to supply a type to create instances.
worker.registerWorkflowImplementationTypes(HelloWorldWorkflowImpl.class);
// Activities are stateless and thread safe, so a shared instance is used.
worker.registerActivitiesImplementations(new FormatImpl());
// Start polling the Task Queue.
factory.start();
}
}

Workflow initiator

There are two ways to start a Workflow, via the Temporal CLI or Temporal SDK. In this tutorial we will use the SDK to start the Workflow which is how most Workflows are started in live environments.

Create InitiateHelloWorld.java and use the SDK to define the start of the Workflow:

src/main/java/helloworldapp/InitiateHelloWorld.java

package helloworldapp;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.serviceclient.WorkflowServiceStubs;
public class InitiateHelloWorld {
public static void main(String[] args) throws Exception {
// This gRPC stubs wrapper talks to the local docker instance of the Temporal service.
WorkflowServiceStubs service = WorkflowServiceStubs.newInstance();
// WorkflowClient can be used to start, signal, query, cancel, and terminate Workflows.
WorkflowClient client = WorkflowClient.newInstance(service);
WorkflowOptions options = WorkflowOptions.newBuilder()
.setTaskQueue(Shared.HELLO_WORLD_TASK_QUEUE)
.build();
// WorkflowStubs enable calls to methods as if the Workflow object is local, but actually perform an RPC.
HelloWorldWorkflow workflow = client.newWorkflowStub(HelloWorldWorkflow.class, options);
// Synchronously execute the Workflow and wait for the response.
String greeting = workflow.getGreeting("World");
System.out.println(greeting);
System.exit(0);
}
}

   Test the app

Let's add a simple unit test to our application to make sure things are working as expected. Test code lives in src/test/java/helloworldapp. Gradle will have generated a default AppTest.java in that location. Remove that file and replace it with HelloWorldWorkflowTest.java that contains the following code:

src/test/java/helloworldapp/HelloWorldWorkflowTest.java

package helloworldapp;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.testing.TestWorkflowEnvironment;
import io.temporal.worker.Worker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class HelloWorldWorkflowTest {
private TestWorkflowEnvironment testEnv;
private Worker worker;
private WorkflowClient workflowClient;
@Before
public void setUp() {
testEnv = TestWorkflowEnvironment.newInstance();
worker = testEnv.newWorker(Shared.HELLO_WORLD_TASK_QUEUE);
worker.registerWorkflowImplementationTypes(HelloWorldWorkflowImpl.class);
workflowClient = testEnv.getWorkflowClient();
}
@After
public void tearDown() {
testEnv.close();
}
@Test
public void testGetGreeting() {
Format format = mock(Format.class);
worker.registerActivitiesImplementations(format);
testEnv.start();
WorkflowOptions options = WorkflowOptions.newBuilder()
.setTaskQueue(Shared.HELLO_WORLD_TASK_QUEUE)
.build();
HelloWorldWorkflow workflow = workflowClient.newWorkflowStub(HelloWorldWorkflow.class, options);
workflow.getGreeting("test");
verify(format).composeGreeting(eq("test"));
}
}

Terminal

From the root of the project, run this command:

./gradlew test

IntelliJ

From within IntelliJ, right click on HelloWorldWorkflowTest and select Run.

Look for "BUILD SUCCESSFUL" in the output to confirm.

   Run the app

At this stage you should have the Temporal server running in a terminal, have the Temporal Web UI open in your browser, and have a project package directory that looks like this:

src/main/java/helloworld/
- Format
- FormatImpl
- InitiateHelloWorld
- HelloWorldWorker
- HelloWorldWorkflow
- HelloWorldWorkflowImpl

An optional step is to add tasks to the build.gradle file so that you can run the main methods from the terminal.

build.gradle

task sayHello(type: JavaExec) {
main = 'helloworldapp.InitiateHelloWorld'
classpath = sourceSets.main.runtimeClasspath
}
task startWorker(type: JavaExec) {
main = 'helloworldapp.HelloWorldWorker'
classpath = sourceSets.main.runtimeClasspath
}

You can start the Workflow and the Worker in any order. If you use the terminal run each command from separate terminal windows.

Start Worker

Terminal

From the project root, run this command:

./gradlew startWorker

IntelliJ

Within IntelliJ, right click on HelloWorldWorker and select Run.

Start Workflow

Terminal

From the project root, run this command:

./gradlew sayHello

IntelliJ

Within IntelliJ, right click on InitiateHelloWorld and select Run.

Congratulations you have successfully built a Temporal application from scratch!

   Lore check

Great work! You now know how to build a Temporal Workflow application using the Java SDK and Gradle. Let's do a quick review to make sure you remember some fo the more important pieces.

One    What are the minimum four pieces of a Temporal Workflow application?

  1. An Activity object and method.
  2. A Workflow object and method.
  3. A Worker to host the Activity and Workflow code.
  4. A function to start the Workflow.

Two    How does the Temporal server get information to the Worker?

It puts information into a Task Queue.

Three    What makes Temporal Activity and Workflow objects different from any other Java object?

The only difference is the interfaces have Temporal decorators.