Workshop 6: Workflow & Activity Separation

Building Multi-Step User Onboarding

Create a comprehensive user onboarding workflow that demonstrates clean separation of concerns across multiple activities


What we want to build

Create a comprehensive user onboarding workflow that demonstrates clean separation of concerns across multiple activities.

This workflow will show how to organize complex business processes into maintainable, testable components.


Expecting Result

By the end of this lesson, you'll have:

  • Multi-step user onboarding workflow with proper orchestration
  • Three distinct activities, each with a single responsibility
  • Proper error handling for critical vs non-critical failures
  • Different timeout configurations for different operation types
  • Clean data modeling with typed result objects

Code Steps

Step 1: Create the Workflow Interface and Data Models

Open class/workshop/lesson_6/workflow/UserOnboardingWorkflow.kt:

package com.temporal.bootcamp.lesson6.workflow

import io.temporal.workflow.WorkflowInterface
import io.temporal.workflow.WorkflowMethod

@WorkflowInterface
interface UserOnboardingWorkflow {

    @WorkflowMethod
    fun onboardUser(email: String): OnboardingResult
}

Data Models

data class OnboardingResult(
    val success: Boolean,
    val userId: String?,
    val message: String,
    val steps: List<String>
)

Clean, typed result objects provide clear contracts


Step 2: Create the Validation Activity

Open class/workshop/lesson_6/activity/UserValidationActivity.kt:

package com.temporal.bootcamp.lesson6.activity

import io.temporal.activity.ActivityInterface
import io.temporal.activity.ActivityMethod

@ActivityInterface
interface UserValidationActivity {

    @ActivityMethod
    fun validateUser(email: String): ValidationResult
}

data class ValidationResult @JsonCreator constructor(
    @JsonProperty("valid") val isValid: Boolean,
    @JsonProperty("errorMessage") val errorMessage: String?
)

Step 3: Implement the Validation Activity

Open class/workshop/lesson_6/activity/UserValidationActivityImpl.kt:

package com.temporal.bootcamp.lesson6.activity

import mu.KotlinLogging
import org.springframework.stereotype.Component

@Component
class UserValidationActivityImpl : UserValidationActivity {

    private val logger = KotlinLogging.logger {}

    override fun validateUser(email: String): ValidationResult {
        logger.info { "Validating user: $email" }

        // Basic email format validation
        val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
        if (!emailRegex.matches(email)) {
            return ValidationResult(false, "Invalid email format")
        }
        // Continued on next slide...

Validation Implementation Continued

        // Check for existing users (simulated)
        val existingUsers = setOf("admin@example.com", "test@example.com")
        if (existingUsers.contains(email.lowercase())) {
            return ValidationResult(false, "Email already registered")
        }

        logger.info { "✅ User validation passed for: $email" }
        return ValidationResult(true, null)
    }
}

Single responsibility: Only handles user validation logic


Step 4: Create the Account Creation Activity

Follow the same pattern for AccountCreationActivity.kt:

package com.temporal.bootcamp.lesson6.activity

import io.temporal.activity.ActivityInterface
import io.temporal.activity.ActivityMethod

@ActivityInterface
interface AccountCreationActivity {

    @ActivityMethod
    fun createAccount(email: String): CreationResult
}

data class CreationResult @JsonCreator constructor(
    @JsonProperty("success") val success: Boolean,
    @JsonProperty("userId") val userId: String?,
    @JsonProperty("errorMessage") val errorMessage: String?
)

Step 5: Implement Account Creation

Create AccountCreationActivityImpl.kt:

package com.temporal.bootcamp.lesson6.activity

import mu.KotlinLogging
import org.springframework.stereotype.Component
import java.util.*

@Component
class AccountCreationActivityImpl : AccountCreationActivity {

    private val logger = KotlinLogging.logger {}

    override fun createAccount(email: String): CreationResult {
        logger.info { "Creating account for: $email" }

        // Simulate database operation
        Thread.sleep(500)

        val userId = "user_${UUID.randomUUID().toString().take(8)}"

        logger.info { "✅ Account created with ID: $userId" }

        return CreationResult(true, userId, null)
    }
}

Step 6: Create the Notification Activity

Create NotificationActivity.kt:

package com.temporal.bootcamp.lesson6.activity

import io.temporal.activity.ActivityInterface
import io.temporal.activity.ActivityMethod

@ActivityInterface
interface NotificationActivity {

    @ActivityMethod
    fun sendWelcomeEmail(email: String, userId: String): NotificationResult
}

data class NotificationResult(
    val sent: Boolean,
    val errorMessage: String?
)

Step 7: Implement Notification

Create NotificationActivityImpl.kt:

package com.temporal.bootcamp.lesson6.activity

import mu.KotlinLogging
import org.springframework.stereotype.Component
import kotlin.random.Random

@Component
class NotificationActivityImpl : NotificationActivity {

    private val logger = KotlinLogging.logger {}

    override fun sendWelcomeEmail(email: String, userId: String): NotificationResult {
        logger.info { "📧 Sending welcome email to: $email" }

        // Simulate email service (with occasional failures)
        Thread.sleep(300)

        if (Random.nextDouble() < 0.1) {
            return NotificationResult(false, "Email service unavailable")
        }

        logger.info { "✅ Welcome email sent successfully" }
        return NotificationResult(true, null)
    }
}

Step 8: Implement the Main Workflow (Part 1)

Open class/workshop/lesson_6/workflow/UserOnboardingWorkflowImpl.kt:

package com.temporal.bootcamp.lesson6.workflow

import com.temporal.bootcamp.lesson6.activity.*
import io.temporal.activity.ActivityOptions
import io.temporal.workflow.Workflow
import java.time.Duration

class UserOnboardingWorkflowImpl : UserOnboardingWorkflow {

    // Different timeouts for different operations
    private val validationActivity = Workflow.newActivityStub(
        UserValidationActivity::class.java,
        ActivityOptions.newBuilder()
            .setStartToCloseTimeout(Duration.ofSeconds(10))
            .build()
    )
    // Continued on next slide...

Main Workflow Implementation (Part 2)

    private val accountCreationActivity = Workflow.newActivityStub(
        AccountCreationActivity::class.java,
        ActivityOptions.newBuilder()
            .setStartToCloseTimeout(Duration.ofSeconds(30))
            .build()
    )

    private val notificationActivity = Workflow.newActivityStub(
        NotificationActivity::class.java,
        ActivityOptions.newBuilder()
            .setStartToCloseTimeout(Duration.ofMinutes(1))
            .build()
    )
    // Continued on next slide...

Notice different timeouts based on operation characteristics


Main Workflow Logic (Part 3)

    override fun onboardUser(email: String): OnboardingResult {
        val logger = Workflow.getLogger(this::class.java)
        val steps = mutableListOf<String>()

        logger.info("Starting onboarding for: $email")

        // Step 1: Validate
        val validation = validationActivity.validateUser(email)
        steps.add("Validation: ${if (validation.isValid) "Passed" else "Failed"}")

        if (!validation.isValid) {
            return OnboardingResult(false, null, validation.errorMessage!!, steps)
        }
        // Continued on next slide...

Main Workflow Logic (Part 4)

        // Step 2: Create Account
        val creation = accountCreationActivity.createAccount(email)
        steps.add("Account: ${if (creation.success) "Created" else "Failed"}")

        if (!creation.success) {
            return OnboardingResult(false, null, creation.errorMessage!!, steps)
        }

        // Step 3: Send Notification (best effort)
        try {
            val notification = notificationActivity.sendWelcomeEmail(email, creation.userId!!)
            steps.add("Email: ${if (notification.sent) "Sent" else "Failed"}")
        } catch (e: Exception) {
            steps.add("Email: Failed (non-critical)")
        }

        logger.info("Onboarding completed successfully")

        return OnboardingResult(true, creation.userId, "User onboarded successfully", steps)
    }
}

Key Design Patterns

Error Handling Strategy:

  • Critical failures (validation, account creation) → Stop process
  • Non-critical failures (email notification) → Log and continue

Timeout Strategy:

  • Quick validation: 10 seconds
  • Database operations: 30 seconds
  • External services: 1 minute

Single Responsibility:

  • ✅ Each activity has one focused job

How to Run

1. Register All Components

worker.registerWorkflowImplementationTypes(UserOnboardingWorkflowImpl::class.java)
worker.registerActivitiesImplementations(
    UserValidationActivityImpl(),
    AccountCreationActivityImpl(), 
    NotificationActivityImpl()
)

Register workflow and all activity implementations


2. Execute the Workflow

val workflow = workflowClient.newWorkflowStub(
    UserOnboardingWorkflow::class.java,
    WorkflowOptions.newBuilder()
        .setTaskQueue("onboarding-queue")
        .setWorkflowId("onboard-${System.currentTimeMillis()}")
        .build()
)

val result = workflow.onboardUser("newuser@example.com")
println("Result: $result")

Create workflow stub and execute with test data


3. Expected Output

Starting onboarding for: newuser@example.com
Validating user: newuser@example.com  
✅ User validation passed for: newuser@example.com
Creating account for: newuser@example.com
✅ Account created with ID: user_abc12345
📧 Sending welcome email to: newuser@example.com
✅ Welcome email sent successfully
Onboarding completed successfully

Clean execution flow with detailed logging


What You've Learned

Key Achievements:

  • How to organize complex workflows with multiple activities
  • Single Responsibility Principle in workflow design
  • Different timeout strategies for different operation types
  • Error handling: critical vs non-critical failures
  • Clean data modeling with typed result objects
  • Best effort operations (notifications can fail without breaking the flow)

🚀 Production-Ready Pattern

This demonstrates a production-ready pattern for complex business processes!

Key Principles Applied:

  • Clean Architecture - Clear separation of concerns
  • Fault Tolerance - Graceful degradation for non-critical failures
  • Maintainability - Each component has a single responsibility
  • Observability - Rich logging and step tracking
  • Type Safety - Strongly typed interfaces and results

Ready for more advanced patterns? Let's continue! 🎉

results matching ""

    No results matching ""