Workshop 15: External Service Integration

Building Production Integration Patterns

A comprehensive user registration workflow that integrates with multiple external services including user profile APIs, payment gateways, databases, and notification systems


What we want to build

A comprehensive user registration workflow that integrates with multiple external services including:

  • User profile APIs via HTTP
  • Payment gateways for processing
  • Databases for data persistence
  • Notification systems (email, SMS)

This demonstrates proper encapsulation of external service calls within Temporal activities.


Expecting Result

A working user registration workflow that:

  • Creates user profiles in external systems via HTTP APIs
  • Processes payments through external payment gateways
  • Saves data to databases
  • Sends multi-channel notifications (email, SMS)
  • Handles failures gracefully with proper error collection
  • Uses different timeout and retry configurations for different service types

Code Steps

Step 1: Define Data Classes for External Integration

Create data classes in workflow/ExternalServiceWorkflow.kt:

data class UserProfile(
    val userId: String,
    val email: String,
    val fullName: String,
    val phoneNumber: String,
    val preferences: Map<String, Any> = emptyMap()
)

data class PaymentRequest(
    val userId: String,
    val amount: Double,
    val currency: String = "USD",
    val paymentMethod: String,
    val description: String
)

data class PaymentResult(
    val transactionId: String,
    val status: String,
    val amount: Double,
    val timestamp: String
)

Step 2: Create Activity Interfaces

Define separate activity interfaces for each external service:

@ActivityInterface
interface UserProfileService {
    @ActivityMethod
    fun createUserProfile(userProfile: UserProfile): String

    @ActivityMethod
    fun getUserProfile(userId: String): UserProfile?

    @ActivityMethod
    fun updateUserProfile(userId: String, updates: Map<String, Any>): Boolean
}

@ActivityInterface
interface PaymentService {
    @ActivityMethod
    fun processPayment(paymentRequest: PaymentRequest): PaymentResult

    @ActivityMethod
    fun refundPayment(transactionId: String, amount: Double): PaymentResult
}

Separate interfaces enable focused responsibility and easier testing


Database and Notification Interfaces

@ActivityInterface
interface DatabaseService {
    @ActivityMethod
    fun saveUserData(userId: String, data: Map<String, Any>): Boolean

    @ActivityMethod
    fun getUserData(userId: String): Map<String, Any>?
}

@ActivityInterface
interface NotificationService {
    @ActivityMethod
    fun sendWelcomeEmail(email: String, userName: String): Boolean

    @ActivityMethod
    fun sendSMS(phoneNumber: String, message: String): Boolean
}

Step 3: Configure Activity Options

Set up different activity options for different service types:

// For external API calls (longer timeout, more retries)
private val externalApiOptions = ActivityOptions.newBuilder()
    .setStartToCloseTimeout(Duration.ofMinutes(5))
    .setRetryOptions(
        RetryOptions.newBuilder()
            .setInitialInterval(Duration.ofSeconds(2))
            .setMaximumInterval(Duration.ofMinutes(1))
            .setBackoffCoefficient(2.0)
            .setMaximumAttempts(5)
            .build()
    )
    .build()

// For internal services (shorter timeout, fewer retries)
private val defaultActivityOptions = ActivityOptions.newBuilder()
    .setStartToCloseTimeout(Duration.ofMinutes(2))
    .setRetryOptions(
        RetryOptions.newBuilder()
            .setInitialInterval(Duration.ofSeconds(1))
            .setMaximumInterval(Duration.ofSeconds(30))
            .setBackoffCoefficient(1.5)
            .setMaximumAttempts(3)
            .build()
    )
    .build()

Workflow Implementation with Service Integration

class UserRegistrationWorkflowImpl : UserRegistrationWorkflow {

    private val userProfileService = Workflow.newActivityStub(
        UserProfileService::class.java, externalApiOptions
    )

    private val paymentService = Workflow.newActivityStub(
        PaymentService::class.java, externalApiOptions
    )

    private val databaseService = Workflow.newActivityStub(
        DatabaseService::class.java, defaultActivityOptions
    )

    private val notificationService = Workflow.newActivityStub(
        NotificationService::class.java, defaultActivityOptions
    )

    override fun registerUser(registrationRequest: UserRegistrationRequest): RegistrationResult {
        val logger = Workflow.getLogger(this::class.java)
        val errors = mutableListOf<String>()
        var userProfileCreated = false
        var paymentProcessed = false

        // Step 1: Create user profile (critical)
        try {
            val userId = userProfileService.createUserProfile(registrationRequest.userProfile)
            userProfileCreated = true
            logger.info("User profile created: $userId")
        } catch (e: Exception) {
            errors.add("Profile creation failed: ${e.message}")
            return RegistrationResult.failed(errors)
        }
        // Continued on next slide...

Service Integration Continued

        // Step 2: Process payment (critical)
        try {
            val paymentResult = paymentService.processPayment(registrationRequest.paymentRequest)
            paymentProcessed = true
            logger.info("Payment processed: ${paymentResult.transactionId}")
        } catch (e: Exception) {
            errors.add("Payment processing failed: ${e.message}")
            // Cleanup: Remove user profile if payment fails
            if (userProfileCreated) {
                try {
                    cleanupUserProfile(registrationRequest.userProfile.userId)
                } catch (cleanupError: Exception) {
                    logger.error("Cleanup failed: ${cleanupError.message}")
                }
            }
            return RegistrationResult.failed(errors)
        }

        // Step 3: Save to database (best effort)
        try {
            databaseService.saveUserData(registrationRequest.userProfile.userId, 
                mapOf("registrationDate" to System.currentTimeMillis()))
        } catch (e: Exception) {
            errors.add("Database save failed: ${e.message}")
            // Continue - not critical
        }

        // Step 4: Send notifications (best effort)
        try {
            notificationService.sendWelcomeEmail(
                registrationRequest.userProfile.email,
                registrationRequest.userProfile.fullName
            )
        } catch (e: Exception) {
            errors.add("Email notification failed: ${e.message}")
            // Continue - not critical
        }

        return RegistrationResult.success(registrationRequest.userProfile.userId, errors)
    }
}

Service Integration Patterns

Error Handling Strategy:

Service Type Failure Impact Strategy
User Profile Critical Fail workflow, no cleanup needed
Payment Critical Fail workflow, cleanup user profile
Database Non-critical Log error, continue workflow
Notifications Non-critical Log error, continue workflow

Timeout Strategy:

  • External APIs: Longer timeouts (5+ minutes)
  • Internal services: Shorter timeouts (2 minutes)
  • Payment gateways: Conservative retries to avoid duplicate charges

Activity Implementation Examples

@Component
class UserProfileServiceImpl : UserProfileService {

    private val httpClient: RestTemplate = RestTemplate()

    override fun createUserProfile(userProfile: UserProfile): String {
        val logger = LoggerFactory.getLogger(this::class.java)

        try {
            val response = httpClient.postForEntity(
                "https://api.userservice.com/profiles",
                userProfile,
                CreateProfileResponse::class.java
            )

            if (response.statusCode.is2xxSuccessful && response.body != null) {
                logger.info("User profile created successfully: ${response.body!!.userId}")
                return response.body!!.userId
            } else {
                throw RuntimeException("Failed to create user profile: ${response.statusCode}")
            }

        } catch (e: Exception) {
            logger.error("User profile creation failed", e)
            throw e
        }
    }
}

💡 Key Integration Patterns

What You've Learned:

  • Service encapsulation in activities with proper interfaces
  • Different timeout strategies for different service types
  • Error classification - critical vs non-critical failures
  • Graceful degradation for non-essential services
  • Compensation patterns for cleanup operations
  • Production-ready external service integration

🚀 Next Steps

You now understand external service integration!

Lesson 16 will cover:

  • Comprehensive testing strategies
  • Production readiness patterns
  • Worker configuration and scaling
  • Monitoring and observability

Ready to deploy to production? Let's continue! 🎉

results matching ""

    No results matching ""