Building a Customer Registration Workflow with AWS Lambda Durable Functions (TypeScript)

Modern user registration flows are no longer simple “insert a record and return 200 OK”. They often involve multiple asynchronous steps, external systems, and human interaction (like email verification).

In this post, we’ll walk through how to build a robust, long-running customer registration workflow using AWS Lambda Durable Functions (Durable Execution SDK) with TypeScript.

The complete code for this demo is available on GitHub: lambda-durable-execution-demo


1. What Are We Trying to Achieve?

We want to build a reliable customer registration process with the following characteristics:

Vimson Varghese

✅ Functional requirements

✅ Non-functional requirements


2. Why AWS Lambda Durable Functions?

Traditional Lambda functions are:

Durable Functions solve this by providing

In short, Durable Functions let you write workflow-style code that:

This makes them perfect for:


3. High-Level Architecture

Here’s what our flow looks like:


4. Starting the Durable Workflow (Register Handler)

This handler accepts the registration request and starts the durable workflow asynchronously.

import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
import { ulid } from 'ulid';

const client = new LambdaClient({});

const postHandler = async (event) => {
  const { body } = event;

  const functionArn = `${process.env['REGISTER_WORKFLOW_FUNCTION_ARN']}:$LATEST`;

  const command = new InvokeCommand({
    FunctionName: functionArn,
    InvocationType: 'Event',
    Payload: JSON.stringify({ ...body }),
    DurableExecutionName: `user-registration-${body.email}-${ulid()}`,
  });

  await client.send(command);

  return {
    statusCode: 201,
    body: JSON.stringify({ message: 'Customer Created' }),
  };
};

Key points


5. The Durable Customer Registration Workflow

This is the heart of the system.

async function customerRegisterWorkflow(input, context) {
  logger.trace('Starting customer registration workflow', { input });

  // Step 1: Create customer
  const customerCreatedStep = await context.step(
    'create-customer-record',
    async () => {
      const customerId = await createCustomer({
        email: input.email,
        firstName: input.firstName,
        lastName: input.lastName,
        passwordHash: input.password,
        status: 'REGISTERED',
      });

      return { customerId };
    }
  );

  // Step 2: Send verification email & wait
  const emailVerificationStep = await context.waitForCallback(
    'send-email-verification',
    async (callbackId) => {
      await sendVerificationEmail(
        { id: customerCreatedStep.customerId, ...input },
        callbackId
      );
    },
    {
      timeout: { seconds: 172800 }, // 48 hours
    }
  );

  // Step 3: Mark verified and complete
  await context.step('send-welcome-email', async () => {
    await updateCustomer({
      id: customerCreatedStep.customerId,
      status: 'EMAIL_VERIFIED',
    });

    return {
      customerId: customerCreatedStep.customerId,
      registrationStatus: 'Successful',
    };
  });
}

Why this works so well


6. Sending the Verification Email

The verification email includes:

When the user clicks the link, we can resume the workflow.


7. Verification Handler (Resuming the Workflow)

This handler is invoked when the user clicks the verification link.

import {
  LambdaClient,
  SendDurableExecutionCallbackSuccessCommand,
} from '@aws-sdk/client-lambda';

const client = new LambdaClient({});

const verifyEmailHandler = async (event) => {
  const { token } = event.queryStringParameters;
  const customerId = verifyEmailVerificationToken(token);

  const customer = await getCustomerbyId(customerId);
  if (!customer) {
    return { statusCode: 404, body: 'Customer Not Found' };
  }

  const command = new SendDurableExecutionCallbackSuccessCommand({
    CallbackId: customer.callbackId,
    Result: JSON.stringify({
      customerId,
      emailVerified: true,
      timestamp: Date.now(),
    }),
  });

  await client.send(command);

  return { statusCode: 200, body: 'Email Verified' };
};

8. Durable Workflow Configuration in serverless framework

  customerRegisterWorkflow:
    handler: src/handlers/customers/register-workflow.handler
    durableConfig:
      executionTimeout: 86400        # 24 hours
      retentionPeriodInDays: 7       # Execution history retained for debugging
    environment:
      TABLE_NAME: !Ref Table
      EMAIL_VERIFICATION_SECRET: ${env:EMAIL_VERIFICATION_SECRET}
      FROM_EMAIL: ${env:FROM_EMAIL}
      API_URL: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com"

What happens here?

No polling. No cron jobs. No custom state handling.


8. Why This Pattern Is Powerful

🚀 Advantages

🧠 Ideal Use Cases


9. Final Thoughts

AWS Lambda Durable Functions allow you to write workflow code that looks synchronous but behaves asynchronously and durably.

In this demo, we:

This pattern scales extremely well and avoids the complexity of Step Functions for code-heavy workflows.