Don’t Rewrite Your Domain: How to Migrate Long-Running Processes to Temporal Safely
There is a dangerous misconception that adopting Temporal requires you to rethink your entire application architecture.
Engineering leaders look at their legacy systems—5-year-old billing scripts, nightly reconciliation jobs, complex provisioning pipelines—and assume that moving them to a durable execution platform means rewriting them from scratch.
They imagine “modeling workflows” means ripping apart the domain logic that has been generating revenue for years.
This stops migration before it starts.
The truth is: You do not need to re-model your domain to get the benefits of durable execution.
In fact, the safest migrations treat domain logic as a “black box.” We don’t rewrite the code that does the work; we simply change the engine that drives it.
Here is the architectural pattern (The Wrapper) we use to migrate long-running processes to Temporal without touching a single line of your core business logic.
The Architecture: Decoupling “Doing” from “Driving”
To migrate without re-modeling, we must distinguish between two types of logic currently mixed in your scripts:
- 1. Domain Logic (The “Doing”):The code that calculates a fee, generates a PDF, or updates a SQL row.This is valuable, battle-tested, and should not be touched.
- 2. Orchestration Logic (The “Driving”):The while loops, sleep() statements, retry counters, and error catching.This is fragile and should be replaced.
In your current scripts, these are tangled together, creating a single point of failure.
The Strategy: The Wrapper Pattern
We treat your existing service methods or scripts as immutable assets. We wrap them in Temporal primitives to give them superpowers (retries, timeouts, visibility) without opening the box.
Step 1: The Activity Wrapper (Keep the Logic)
Let’s say you have a legacy method ProcessMonthlyBilling(userId). It’s 500 lines of complex Python or Go. It works.
Do not refactor it.
Instead, create a Temporal Activity that acts as a dumb pass-through. Crucially, we implement heartbeating to ensure that long-running legacy processes don’t time out.
// The Activity Wrapper
// It bridges the gap between the orchestration engine and your legacy code.
func (a *BillingActivities) ProcessBillingActivity(ctx context.Context, userID string) error {
// 1. Setup: Instantiate your existing, battle-tested service
legacyService := billing.NewLegacyService()
// 2. HEARTBEAT (Critical for Long-Running Processes)
// Since your legacy logic might run for 10+ minutes, we must
// tell Temporal we are still alive so it doesn’t timeout.
// This allows us to wrap very slow legacy processes safely.
go func() {
for {
select {
case <-ctx.Done():
return
case <-time.After(30 * time.Second):
activity.RecordHeartbeat(ctx, “Legacy process still running…”)
}
}
}()
// 3. Execution: Call the EXACT same method you use today.
// We are not re-modeling the billing logic. We are just running it.
return legacyService.ProcessMonthlyBilling(userID)
}
The Win: You haven’t introduced bugs into the billing logic because you haven’t touched the billing logic.
Step 2: The Workflow Orchestrator (Replace the Script)
Currently, your “orchestration” might be a cron script that iterates through a list of users. If that script crashes on User #4,000, you lose your place.
We replace that loop with a Temporal Workflow, which manages the state and orchestration, calling the wrapped activity we just created.
// Define the “Policy” – How do we handle legacy instability?
options := workflow.ActivityOptions{
StartToCloseTimeout: time.Minute * 30, // Allow time for legacy slowness
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
MaximumAttempts: 3,
// Crucial: Don’t retry logic errors, only transient infrastructure errors
NonRetryableErrorTypes: []string{“DomainLogicError”, “InvalidUser”},
},
}
ctx = workflow.WithActivityOptions(ctx, options)for _, userID := range userIDs {
// Execute the wrapper activity.
// Temporal automatically persists the cursor state of ‘i’.
// If the worker crashes here, it resumes right here.
err := workflow.ExecuteActivity(ctx, a.ProcessBillingActivity, userID).Get(ctx, nil)if err != nil {
// Log failure but continue to next user (Saga pattern)
logger.Error(“Billing failed for user”, “id”, userID)
}
}
return nil
}
The Operational Upgrade:
The logic inside ProcessMonthlyBilling is exactly the same. But now, if the server crashes, the Workflow resumes execution at the exact user it left off on. You have gained durability without re-modeling your data or your domain.
The Technical Safety Net: Idempotency
There is one rule for this pattern: Idempotency.
Temporal guarantees that your Activity will run at least once. In rare network failure scenarios, it might try to run your wrapper twice.
- Ideal: Your legacy ProcessMonthlyBilling checks if already_billed: return.
- Wrapper Fix: If you cannot touch the legacy code, put the check in the wrapper:
// 1. Instantiate existing legacy service
legacyService := billing.NewService()
// 2. Call the legacy method directly
err := legacyService.ProcessMonthlyBilling(userID)
// 3. The “Wrapper” Logic: Interpret the error, don’t prevent it.
if err != nil {
// If the legacy system throws a “Duplicate Entry” or “Already Paid” error,
// we treat that as a SUCCESS in Temporal (idempotency achieved).
if IsDuplicateTransactionError(err) {
return nil // Swallow the error, return success
}
// Real failures (DB down, Network timeout) should still bubble up
// so Temporal can retry them.
return err
}
return nil
}
This ensures that even if you don’t re-model the domain, you protect it from side effects.
Why This Pattern Wins for Platform Teams
- 1. Risk Reduction:You are not rewriting business logic. You are wrapping it. The regression risk is near zero.
- 2. Immediate Visibility:You instantly get a Temporal UI showing exactly which step every process is in. No more log-diving to find “where the script died.”
- 3. Speed:We see teams ship their first migrated workflow in days, not months, because they bypass the architectural debates that stall greenfield projects.
Start Your Beachhead Migration
Migration isn’t about rewriting your stack. It’s about securing the execution path.
At Xgrid, our Forward-Deployed Engineers use this exact Wrapper Pattern to help teams move their first critical workflow—from “cron chaos” to Temporal predictability—without re-modeling their domain.
Ready to wrap and migrate your first workflow?
We offer a structured Beachhead Workflow Sprint to identify your best migration candidate, wrap it, and ship it to production in weeks.




