Building Workflows

with Temporal.io



Michał Samluk
Principal Engineer, Architect @ MAXIO

Our Story

2021: Chargify and SaaSOptics receive $150 million investment from Batter Ventures
2022: Chargify and SaaSOptics are Becoming Maxio

Our stack

  • Advanced Billing - Ruby on Rails
  • Maxio Core - Django
  • Maxio Payments
  • Other services

Product Evolution

Consolidate UX
Consolidate common components
Cross-platform flows

An example flow

An example flow

Our Problem

How our components should interact?

Async + orchestrator

How?

Durable Execution Platform

Our choice: Temporal.io

Works on both stacks Ruby & Python

Ruby unofficial sdk

https://github.com/coinbase/temporal-ruby

New official SDK!

Active Development!
https://github.com/temporalio/sdk-ruby/
https://github.com/temporalio/sdk-core/

Non-functional requirements

Resiliency

  • Ability to recover quickly
  • Tolerance to system failures
  • Automatic retries and compensation
  • Minimizes service downtime

How?

SAGAS

  • No monolithic transaction
  • Manages distributed transactions
  • Isolated steps
  • Compensating transactions

SAGA Orchestrator

Backward recovery

Defining workflow

              
class MyWorkflow < Temporal::Workflow
  def execute
    # Here you will execute your activities
  end
end
              
            
              
Temporal.start_workflow(MyWorkflow)
              
            

Defining action

              
class HelloWorldActivity < Temporal::Activity
  def execute(name)
    text = "Hello World, #{name}"

    puts text

    return text
  end
end
              
            

Simple workflow

              
class RenewSubscriptionWorkflow < Temporal::Workflow
  def execute(user_id)
    subscription = FetchUserSubscriptionActivity.execute!(user_id)
    subscription ||= CreateUserSubscriptionActivity.execute!(user_id)

    ChargeCreditCardActivity.execute!(subscription[:price], subscription[:card_token])

    RenewedSubscriptionActivity.execute!(subscription[:id])
    SendSubscriptionRenewalEmailActivity.execute!(user_id, subscription[:id])
  end
end
              
            

Compensating activities

              
class RenewSubscriptionWorkflow < Temporal::Workflow
  def execute(user_id)
    subscription = FetchUserSubscriptionActivity.execute!(user_id)
    subscription ||= CreateUserSubscriptionActivity.execute!(user_id)

    ChargeCreditCardActivity.execute!(subscription[:price], subscription[:card_token])

    # ...
  rescue CreditCardNotChargedError => e
    CancelSubscriptionActivity.execute!(subscription[:id])
    SendSubscriptionCancellationEmailActivity.execute!(user_id, subscription[:id])
  end
end
              
            

Forward recovery

              
class RetryableActivity < Temporal::Activity
  retry_policy(
    interval: 1,
    backoff: 1,
    max_attempts: 3,
    non_retriable_errors: [NonRetriableError]
  )

  def execute(user_id)
    # ...
  end
end
              
            

Code Maintainability - what avoid

  • Highly complex flow
  • Long running process (transaction)
  • Coupling between different domains
  • Handling errors, retries, custom rollbacks
  • Unclear execution path

Temporal makes it is easy

  • Activities - local atomic transactions
  • Less complex logic to handle failures, rollbacks
  • Workflows - declarative code
  • No need to manage the state

Visibility

Event Sourcing

Traceability

Allows to build custom propagators

Scalability

Scales horizontaly workers

Seperate workers for activity and workflow tasks

Recap

  • Resiliency
  • State managment
  • Code maintainability
  • Traceability
  • Scalability

Beyond the scope

  • Scheduled execution / Timers
  • Long running workflows
  • Signals
  • Temporal self-hosted vs cloud
  • And more...

Thank you