Skip to main content

Build a Financial Transaction Application

You'll build a money movement app that withdraws from one account, deposits to another, and refunds if the deposit fails. You'll run it against a local Temporal server and watch it complete in the UI.

Before you begin

Quickstart Guide

Run through the Quickstart to set up your local environment.

1. Clone the project

git clone https://github.com/temporalio/money-transfer-project-template-go
cd money-transfer-project-template-go
tip

Each repo is a GitHub Template. Clone it to your own account as a starting point for your own application.

2. Explore the code

A Temporal application has three parts: a Workflow that orchestrates the steps, Activities that do the actual work, and a Worker that runs both.

Workflow

The Workflow takes a PaymentDetails input (source account, target account, amount, reference ID) defined in a shared file, and uses it to call each Activity in turn. Walk through the Workflow code step by step. Select your SDK in the tab row, then use Next to move through each piece.

All snippets below come from your Workflow file:

Before calling any Activity, the Workflow defines a retry policy. Temporal retries failed Activities automatically. This policy controls how many times, how long to wait between attempts, and which errors should never be retried.

retrypolicy := &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: 100 * time.Second,
MaximumAttempts: 500, // 0 = unlimited
NonRetryableErrorTypes: []string{"InvalidAccountError", "InsufficientFundsError"},
}

options := workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
RetryPolicy: retrypolicy,
}
ctx = workflow.WithActivityOptions(ctx, options)

BackoffCoefficient: 2.0 means Temporal doubles the wait between each attempt: 1s, 2s, 4s, up to the MaximumInterval cap.

Don't Retry

  • InvalidAccountError - Wrong account number
  • InsufficientFundsError - Not enough money

These are business logic errors that won't be fixed by retrying.

Retry Automatically

  • Network timeouts - Temporary connectivity
  • Service unavailable - External API down
  • Rate limiting - Too many requests

These are temporary issues that often resolve themselves.

Activities

Activities do the real work. Each one calls an external service. If an Activity fails, Temporal retries it. The three Activities in this app: Withdraw, Deposit, and Refund.

Each Activity is a plain function or method that calls an external service. There's no Temporal-specific orchestration logic. The Withdraw Activity debits the source account and returns a confirmation string.

activity.go

func Withdraw(ctx context.Context, data PaymentDetails) (string, error) {
log.Printf("Withdrawing $%d from account %s.\n\n",
data.Amount,
data.SourceAccount,
)

referenceID := fmt.Sprintf("%s-withdrawal", data.ReferenceID)
bank := BankingService{"bank-api.example.com"}
confirmation, err := bank.Withdraw(data.SourceAccount, data.Amount, referenceID)
return confirmation, err
}

Worker

The Worker is the process that runs your Workflow and Activity code. It connects to Temporal, polls a Task Queue, and executes tasks as they arrive. The Task Queue name must match what the Client uses to start the Workflow. A mismatch silently prevents execution.

worker/main.go

func main() {

c, err := client.Dial(client.Options{})
if err != nil {
log.Fatalln("Unable to create Temporal client.", err)
}
defer c.Close()

w := worker.New(c, app.MoneyTransferTaskQueueName, worker.Options{})

// This worker hosts both Workflow and Activity functions.
w.RegisterWorkflow(app.MoneyTransfer)
w.RegisterActivity(app.Withdraw)
w.RegisterActivity(app.Deposit)
w.RegisterActivity(app.Refund)

// Start listening to the Task Queue.
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("unable to start Worker", err)
}
}

Client

The Client is what starts a Workflow Execution. It connects to Temporal, submits the request with a unique Workflow ID and Task Queue name, then waits for the result. The Workflow ID makes each execution idempotent. Submitting the same ID twice returns the existing execution instead of starting a new one.

start/main.go

func main() {
// Create the client object just once per process
c, err := client.Dial(client.Options{})

if err != nil {
log.Fatalln("Unable to create Temporal client:", err)
}

defer c.Close()

input := app.PaymentDetails{
SourceAccount: "85-150",
TargetAccount: "43-812",
Amount: 250,
ReferenceID: "12345",
}

options := client.StartWorkflowOptions{
ID: "pay-invoice-701",
TaskQueue: app.MoneyTransferTaskQueueName,
}

log.Printf("Starting transfer from account %s to account %s for %d", input.SourceAccount, input.TargetAccount, input.Amount)

we, err := c.ExecuteWorkflow(context.Background(), options, app.MoneyTransfer, input)
if err != nil {
log.Fatalln("Unable to start the Workflow:", err)
}

log.Printf("WorkflowID: %s RunID: %s\n", we.GetID(), we.GetRunID())

var result string

err = we.Get(context.Background(), &result)

if err != nil {
log.Fatalln("Unable to get Workflow result:", err)
}

log.Println(result)
}

3. Run the application

You need three terminals. Start them in order.

Terminal 1: Temporal server

Start the local Temporal server. This runs the scheduler, state store, and Web UI at localhost:8233. The --db-filename flag persists Workflow state to disk, which is required for the crash recovery demo in Part 2.

Port conflict

If you get an address already in use error, a previous process may still hold port 7233. Run lsof -ti:7233 | xargs kill on macOS/Linux to clear it.

temporal server start-dev --db-filename ./temporal.db

Terminal 2: Worker

Start the Worker. It connects to Temporal, polls the Task Queue, and executes your Workflow and Activity code. Leave it running.

Java: first-run compile time

Maven downloads the Temporal SDK and its dependencies on the first run. This can take a few minutes on a fresh machine. Subsequent runs compile from cache and start in seconds.

go run worker/main.go

Terminal 3: Start the Workflow

Run the client to submit a Workflow Execution. You should see a transfer result printed when it completes.

go run start/main.go
Transfer complete (transaction IDs: W1779185060, D1779185060)
Workflow Status: COMPLETED
Withdraw Activity: COMPLETED
Deposit Activity: COMPLETED
Transaction: COMPLETED

4. View in the Temporal UI

The Temporal Web UI shows the full event history: every Activity scheduled, started, and completed, plus the Workflow input and result.

Open Temporal UI

View your Workflow Execution at localhost:8233

Temporal Web UI showing a completed money transfer Workflow

Click on the Workflow in the list to see the full event history. In Part 2, you'll use that history to debug a live failure.