Brewing Coffee : First Temporal Workflow, One Cup at a Time

AniruddhaAniruddha
4 min read

Welcome back to our coffee-fueled backend journey! ☕

In the last post, we got the Temporal setup brewing for our Best-Coffee-Shop—we laid the foundation.

Now, it’s time to write our first real workflow: the one that brings a customer’s coffee order to life. Think of this workflow as the café manager—it knows the customer’s order, makes sure payment happens, coordinates the kitchen to brew the coffee, and ensures it’s served piping hot.

In a microservice world, each service is responsible for a separate concern. Services often communicate via REST APIs or event-based messaging. Temporal offers a different approach—treating each workflow as an application flow, and each activity as a building block (like an individual micro-service).

Temporal handles:

  • Communication between the Workflow and Activities

  • Failures and retries

  • Persistence of state

Each step in the workflow is usually represented by an Activity, though technically, a Workflow can exist without any Activities.

Our Coffee Workflow

When a customer places an order, we’ll start a Workflow with the order info. Here’s what happens step-by-step:

  1. Calculate the billing amount and wait for payment.

    Our (future) payment-service will handle bill calculation and inform the workflow once payment is received.

  2. Gather ingredients, materials, and containers.

    The inventory-service will ensure we have everything needed to serve the order.

  3. Brew the coffee and package it.

    The brewing-service will take care of the hot stuff.

  4. Serve it to the customer.

    The counter-service will handle that final delivery.

Each service receives input data from the workflow and returns output. The workflow coordinates everything—passing data from one step to the next.

Setup the Orchestrator service

We already set up the Temporal cluster in the previous post. Now let’s build our first service: orchestrator-service.

best-coffee-shop> mkdir orchestrator-service
best-coffee-shop> cd orchestrator-service
best-coffee-shop/orchestrator-service> touch workflow.py main.py

service will have two files:

file workflow.py

from temporalio import workflow
from datetime import timedelta
from dataclasses import dataclass


@dataclass  
class CoffeeOrder:
    customer_name: str
    order_number: str
    quantity: int = 1  


@workflow.defn
class CoffeeOrderWorkflow:
    @workflow.run
    async def run(self, order: CoffeeOrder):
        workflow.logger.info("CoffeeOrderWorkflow : we soon be serving coffee.. Order : {0}".format(order))

The workflow receives a CoffeeOrder. Our shop doesn’t do customizations yet—we only care about how many cups you want. We use @dataclass to model the order, and @workflow.defn to let Temporal know this class defines a workflow. The run method (decorated with @workflow.run) is where we’ll build our workflow logic. For now, we’re just logging the order.

file main.py

from temporalio.worker import Worker
from temporalio.client import Client
from workflow import CoffeeOrderWorkflow
import asyncio

TEMPORAL_ADDRESS = 'temporal:7233'
TASK_QUEUE = 'best-coffee-orders'

async def main():
    client = await Client.connect(TEMPORAL_ADDRESS)
    worker = Worker(
        client,
        task_queue=TASK_QUEUE,
        workflows=[CoffeeOrderWorkflow],
    )
    await worker.run()

if __name__ == "__main__":
    asyncio.run(main())

The client connects to Temporal and registers the CoffeeOrderWorkflow with a worker.

A worker listens to the task queue and executes workflows or activities assigned to it.

Think of a Worker as a background process that continuously listens for tasks on a Task Queue. When a Workflow is executed, Temporal creates a task and places it in the appropriate queue. The Worker picks up the task, runs it, and then sends the result back to the Temporal server.

A few key points:

  • Every Workflow is associated with a specific Task Queue.

  • A Task Queue can have multiple Workers, which allows you to scale horizontally and improve fault tolerance.

Here’s the flow in simple terms:

Workflow starts ➜ 
    Temporal creates a task ➜ 
        Task is placed in the queue ➜ 
            Worker polls the queue ➜ 
                Task is picked up and executed 
                    ➜ Result is returned to Temporal.

Let’s containerize Orchestrator service

To run orchestrator-service in Docker, we’ll need:

  • init.sh: startup script

  • requirements.txt: Python dependencies

  • Updates to docker-compose.yml

file : init.sh

#!/usr/bin/env sh
pip install -r /requirements.txt
python /app/main.py

file : `requirements.txt

temporalio

Add following to docker-compose.yml

  .....
  orchestrator-service:
    container_name: orchestrator-service
    image: python:3
    depends_on:
      - temporal
    volumes:  
      - ./orchestrator-service:/app  
      - ./requirements.txt:/requirements.txt  
      - ./init.sh:/init.sh
    entrypoint: sh -c "chmod -R 755 /app && chmod -R 755 /init.sh && sh /init.sh"
    networks:
      - best-coffee-network

Try It Out

Run:

best-coffee-shop> docker-compose up

Visit http://localhost:8080 (Temporal UI). It should look something like this

Click Start Workflow → by providing following

Workflow ID     = best-coffee-1
Task Queue      = best-coffee-orders
Workflow Type   = CoffeeOrderWorkflow
Data            = {"customer_name": "Aniruddha", "order_number": "best-coffee-1", "quantity": 1}
Encoding        = json/plain

** Task Queue & Workflow Type must match to our code.

Once started, you’ll see the workflow on the UI dashboard something like

Code for this post

You can find the code up to this 👉: Code repo

0
Subscribe to my newsletter

Read articles from Aniruddha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aniruddha
Aniruddha