Adding a Simple Activity
Lesson 5: Understanding Workflow-Activity Pattern
Learn how workflows orchestrate business processes while activities perform the actual work, and how they communicate through Temporal's infrastructure.
Objective
By the end of this lesson, you will understand:
- ✅ Workflow-Activity relationship and division of responsibilities
- ✅ Activity Stubs - the magic bridge between workflows and activities
- ✅ Activity Options and proper timeout configuration
- ✅ Logging and observability best practices
- ✅ Error handling basics for resilient workflows
1. Workflow-Activity Relationship
Division of Responsibilities
Workflow: Orchestration and Coordination
class CalculatorWorkflowImpl : CalculatorWorkflow {
override fun add(a: Int, b: Int): Int {
// Log the operation
// Call activity
// Handle result
return mathActivity.performAddition(a, b)
}
}
Workflow = Conductor of an Orchestra
Activity: Actual Work and Business Logic
class MathActivityImpl : MathActivity {
override fun performAddition(a: Int, b: Int): Int {
// Do the actual calculation
// Handle any complex logic
// Access external systems if needed
return a + b
}
}
Activity = Musicians doing the real work
Why This Separation?
Key Benefits:
- ✅ Reliability: If an activity fails, only that step needs to retry
- ✅ Scalability: Activities can be distributed across different workers
- ✅ Maintainability: Business logic is isolated and testable
- ✅ Flexibility: Different activities can have different retry policies
Clean separation = robust distributed systems
2. Activity Stubs - The Magic Bridge
What is an Activity Stub?
An activity stub is a proxy that makes activity calls look like regular method calls while handling all the Temporal infrastructure behind the scenes.
// Creating a stub
private val mathActivity = Workflow.newActivityStub(
MathActivity::class.java,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.build()
)
// Using the stub (looks like a normal method call!)
val result = mathActivity.performAddition(5, 3)
What Happens Behind the Scenes
Workflow ──► Activity Stub ──► Temporal Server ──► Activity Queue
▲ │
│ ▼
└── Result ◄── Temporal Server ◄── Worker ◄── Activity Implementation
The Magic Steps:
- Workflow calls stub method
- Temporal serializes the call and parameters
- Temporal queues the activity task
- Worker picks up the activity task
- Worker executes the actual activity implementation
- Temporal returns the result to the workflow
- Workflow continues with the result
3. Activity Options and Configuration
Essential Timeout Settings
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30)) // Max execution time
.setScheduleToCloseTimeout(Duration.ofMinutes(5)) // Max total time (including retries)
.setScheduleToStartTimeout(Duration.ofMinutes(1)) // Max time in queue
.build()
Configure timeouts based on your operation's needs!
Timeout Types Explained
Three Critical Timeout Types:
- StartToCloseTimeout: How long the activity can run once it starts
- ScheduleToCloseTimeout: Total time allowed (including retries and queue time)
- ScheduleToStartTimeout: How long it can wait in the queue before starting
Think of it as setting expectations for performance!
Choosing Appropriate Timeouts
// Quick operations
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(10))
.build()
// Database operations
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.build()
// External API calls
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofMinutes(2))
.build()
// File processing
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofMinutes(10))
.build()
4. Logging and Observability
Workflow Logging
// In workflows, use Workflow.getLogger()
val logger = Workflow.getLogger(this::class.java)
logger.info("Workflow started with parameters: $a, $b")
Activity Logging
// In activities, use regular logging
private val logger = KotlinLogging.logger {}
logger.info { "Activity processing: $a + $b" }
Why Different Logging?
Key Differences:
- Workflow logs are part of the workflow history and are replayed
- Activity logs are immediate and not replayed
- Temporal Web UI shows workflow logs in the execution timeline
- Activity logs appear in your application logs
Use the right logging approach for the right component!
5. Error Handling Basics
Activity Failures
@Component
class MathActivityImpl : MathActivity {
override fun performAddition(a: Int, b: Int): Int {
// Simulate a failure condition
if (a < 0 || b < 0) {
throw IllegalArgumentException("Negative numbers not supported")
}
return a + b
}
}
Activities can fail, and that's okay!
Workflow Error Handling
class CalculatorWorkflowImpl : CalculatorWorkflow {
override fun add(a: Int, b: Int): Int {
return try {
mathActivity.performAddition(a, b)
} catch (e: Exception) {
// Workflow can handle activity failures
logger.warn("Addition failed: ${e.message}")
0 // Default value or alternative logic
}
}
}
Workflows can gracefully handle activity failures!
Best Practices
✅ Activity Design
1. Keep Activities Focused
// Good: Single responsibility
@ActivityMethod
fun calculateSum(numbers: List<Int>): Int
@ActivityMethod
fun validateInput(data: String): Boolean
// Bad: Multiple responsibilities
@ActivityMethod
fun calculateSumAndValidateAndLog(data: String): Int
More Activity Best Practices
2. Make Activities Idempotent
override fun createUser(userData: UserData): String {
// Check if user already exists
val existing = userRepository.findByEmail(userData.email)
if (existing != null) {
return existing.id // Safe to call multiple times
}
return userRepository.create(userData).id
}
Activities should be safe to retry!
Even More Activity Best Practices
3. Use Descriptive Method Names
// Good: Clear what the activity does
fun performAddition(a: Int, b: Int): Int
fun validateCreditCard(cardNumber: String): ValidationResult
fun sendWelcomeEmail(userId: String): EmailResult
// Bad: Vague names
fun doWork(data: Any): Any
fun process(input: String): String
Make your intent crystal clear!
✅ Workflow Design
1. Keep Workflows Simple
// Good: Simple orchestration
override fun processOrder(order: Order): OrderResult {
val payment = paymentActivity.processPayment(order)
val fulfillment = fulfillmentActivity.shipOrder(order)
return OrderResult(payment, fulfillment)
}
// Bad: Complex business logic in workflow
override fun processOrder(order: Order): OrderResult {
val discountedPrice = order.price * 0.9 // This should be in an activity
// ...
}
Workflows orchestrate, activities execute!
Workflow Timeout Strategy
2. Use Meaningful Timeouts
// Consider the actual operation when setting timeouts
private val quickActivity = Workflow.newActivityStub(
QuickActivity::class.java,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(5))
.build()
)
private val slowActivity = Workflow.newActivityStub(
SlowActivity::class.java,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofMinutes(5))
.build()
)
❌ Common Mistakes
1. Calling Activities Directly
// Bad: Direct instantiation
class BadWorkflowImpl : MyWorkflow {
override fun process(): String {
val activity = MathActivityImpl() // Wrong!
return activity.performAddition(1, 2)
}
}
// Good: Use stub
class GoodWorkflowImpl : MyWorkflow {
private val activity = Workflow.newActivityStub(...)
override fun process(): String {
return activity.performAddition(1, 2) // Correct!
}
}
More Common Mistakes
2. No Timeout Configuration
// Bad: No timeouts specified
private val activity = Workflow.newActivityStub(
MyActivity::class.java
)
// Good: Explicit timeouts
private val activity = Workflow.newActivityStub(
MyActivity::class.java,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.build()
)
Always specify timeouts explicitly!
Final Common Mistake
3. Complex Logic in Workflows
// Bad: Business logic in workflow
override fun calculateDiscount(order: Order): Double {
var discount = 0.0
if (order.amount > 100) {
discount = 0.1
}
// Complex calculations should be in activities
return discount
}
// Good: Delegate to activity
override fun calculateDiscount(order: Order): Double {
return discountActivity.calculateDiscount(order)
}
💡 Key Takeaways
What You've Learned:
- ✅ Workflows orchestrate, activities execute business logic
- ✅ Activity stubs handle all Temporal infrastructure magic
- ✅ Timeout configuration is critical for reliability
- ✅ Proper logging provides visibility into execution
- ✅ Error handling enables graceful failure recovery
- ✅ Best practices ensure maintainable code