temporal-workflow

star 0

Patterns for writing Temporal workflows and activities in Go for the payment-processing service. Use when implementing new workflows, adding activities, handling signals/queries, writing retry logic, or testing Temporal code. Triggers on tasks involving workflow orchestration, durable execution, or payment processing logic.

SDBurt By SDBurt schedule Updated 1/17/2026

name: temporal-workflow description: Patterns for writing Temporal workflows and activities in Go for the payment-processing service. Use when implementing new workflows, adding activities, handling signals/queries, writing retry logic, or testing Temporal code. Triggers on tasks involving workflow orchestration, durable execution, or payment processing logic.

Temporal Workflow Development

Patterns for this payment-processing service built with Go and Temporal SDK.

Core Rules

Determinism in Workflows

Workflow code must be deterministic. On replay, the same inputs must produce the same outputs.

Allowed in workflows:

  • workflow.Now(ctx) for current time
  • workflow.GetLogger(ctx) for logging
  • workflow.SideEffect() for one-time non-deterministic values
  • workflow.ExecuteActivity() for side effects

Forbidden in workflows:

  • time.Now(), rand.Int(), uuid.New()
  • Direct database/API calls
  • Global mutable state
// WRONG
id := uuid.New().String()

// CORRECT
var id string
workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
    return uuid.New().String()
}).Get(&id)

Activity Configuration

Standard activity options for this project:

actOpts := workflow.ActivityOptions{
    StartToCloseTimeout: 30 * time.Second,
    RetryPolicy: &temporal.RetryPolicy{
        InitialInterval:    time.Second,
        BackoffCoefficient: 2.0,
        MaximumAttempts:    3,
        NonRetryableErrorTypes: []string{
            "HardDeclineError",
            "FraudError",
            "ValidationError",
        },
    },
}
ctx = workflow.WithActivityOptions(ctx, actOpts)

For database operations, use shorter timeouts:

actOpts := workflow.ActivityOptions{StartToCloseTimeout: 10 * time.Second}

Query Handlers

Expose workflow state for external visibility:

func PaymentWorkflow(ctx workflow.Context, req PaymentRequest) (*PaymentResult, error) {
    state := &PaymentState{PaymentID: req.PaymentID, Status: StatusPending}

    _ = workflow.SetQueryHandler(ctx, "get-status", func() (*PaymentState, error) {
        return state, nil
    })

    _ = workflow.SetQueryHandler(ctx, "get-attempts", func() ([]AttemptRecord, error) {
        return state.Attempts, nil
    })
    // ... workflow logic
}

Query from API:

resp, err := temporalClient.QueryWorkflow(ctx, workflowID, "", "get-status")
var state PaymentState
resp.Get(&state)

Signal Handlers

Handle external events during workflow execution:

const (
    SignalUpdatePaymentMethod = "update-payment-method"
    SignalCancelPayment       = "cancel-payment"
)

func PaymentWorkflow(ctx workflow.Context, req PaymentRequest) (*PaymentResult, error) {
    updateMethodCh := workflow.GetSignalChannel(ctx, SignalUpdatePaymentMethod)
    cancelCh := workflow.GetSignalChannel(ctx, SignalCancelPayment)

    // Non-blocking check pattern
    selector := workflow.NewSelector(ctx)
    selector.AddReceive(cancelCh, func(c workflow.ReceiveChannel, _ bool) {
        var reason string
        c.Receive(ctx, &reason)
        // Handle cancellation
    })
    selector.AddDefault(func() {}) // Non-blocking
    selector.Select(ctx)
}

Send signal from API:

err := temporalClient.SignalWorkflow(ctx, workflowID, "", SignalCancelPayment, "user_requested")

Timer with Signal Handling

Wait for duration while remaining responsive to signals:

func waitWithSignals(ctx workflow.Context, duration time.Duration, cancelCh workflow.ReceiveChannel) error {
    timer := workflow.NewTimer(ctx, duration)
    selector := workflow.NewSelector(ctx)
    cancelled := false

    selector.AddFuture(timer, func(f workflow.Future) {
        // Timer completed
    })

    selector.AddReceive(cancelCh, func(c workflow.ReceiveChannel, _ bool) {
        var reason string
        c.Receive(ctx, &reason)
        cancelled = true
    })

    selector.Select(ctx)

    if cancelled {
        return fmt.Errorf("cancelled")
    }
    return nil
}

Child Workflows

For scheduled/recurring payments, spawn child workflows:

childOpts := workflow.ChildWorkflowOptions{
    WorkflowID: fmt.Sprintf("payment-%s-%d", scheduleID, paymentNum),
}
ctx = workflow.WithChildOptions(ctx, childOpts)

var result PaymentResult
err := workflow.ExecuteChildWorkflow(ctx, PaymentWorkflow, paymentReq).Get(ctx, &result)

Error Classification

Use typed errors for retry policy control:

// In activity - non-retryable error
return nil, temporal.NewApplicationError("Card stolen", "HardDeclineError")

// In activity - soft decline (let workflow decide retry)
return &ChargeResult{Success: false, DeclineCode: "insufficient_funds"}, nil

See references/patterns.md for decline classification and retry timing strategies.

Testing

Use Temporal test framework with mocked activities:

func TestPaymentWorkflow_Success(t *testing.T) {
    testSuite := &testsuite.WorkflowTestSuite{}
    env := testSuite.NewTestWorkflowEnvironment()

    env.OnActivity(activities.ValidatePayment, mock.Anything, mock.Anything).
        Return(&ValidationResult{Valid: true}, nil)

    env.OnActivity(activities.ProcessCardPayment, mock.Anything, mock.Anything).
        Return(&ChargeResult{Success: true, TransactionID: "txn_123"}, nil)

    env.ExecuteWorkflow(PaymentWorkflow, PaymentRequest{...})

    require.True(t, env.IsWorkflowCompleted())
    require.NoError(t, env.GetWorkflowError())
}

Test signals with delayed callbacks:

env.RegisterDelayedCallback(func() {
    env.SignalWorkflow(SignalCancelPayment, "user_requested")
}, 30*time.Minute)

See references/testing.md for complete testing patterns.

Install via CLI
npx skills add https://github.com/SDBurt/payment-processing --skill temporal-workflow
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator