22. Pattern: Draft Workflow Resource

The Draft Workflow Resource pattern manages multi-step processes by capturing and persisting state inside a dedicated “draft” resource instance before executing a final completion action.

Instead of requiring clients to submit massive, fully populated payloads in a single request or relying on fragile client-side session state, this pattern allows consumers to accumulate workflow data progressively across multiple HTTP calls (POST, PATCH, GET).

Once all necessary information is gathered, the client executes a functional completion endpoint (e.g., POST /loan-applications/{id}/submit) that validates the complete draft and provisions all underlying domain entities atomically.

22.1. Overview

Many enterprise business procedures—such as loan applications, user onboarding, or complex multi-step checkout flows—require gathering information in stages.

The Draft Workflow Resource pattern separates the data accumulation stage from the final business transaction stage:

  1. Initiate Workflow (POST /workflow-drafts): The client creates a new workflow session instance. The server returns a unique draft identifier (draftId) and initializes a draft resource in a state such as DRAFT or IN_PROGRESS.
  2. Progressive Accumulation (PATCH /workflow-drafts/{id}): The client updates the draft step-by-step as users complete UI forms. The server validates incoming partial payloads against step-specific rules while allowing incomplete overall state to be saved.
  3. Inspect Progress (GET /workflow-drafts/{id}): The client or server can retrieve current draft progress, including completion percentages, missing required fields, or validation warnings.
  4. Final Completion / Submission (POST /workflow-drafts/{id}/submit): A functional action endpoint validates that all required workflow stages are complete, freezes the draft, and atomically creates the downstream production entities (e.g., creating a Loan, an UnderwritingCase, and a BorrowerProfile).

22.2. When to Use Draft Workflow Resource Pattern

Use Draft Workflow Resource when:

  • A business process spans multiple screens, steps, or user sessions.
    • e.g., Commercial mortgage applications, multi-party contract builder wizards, or complex insurance quote questionnaires.
  • Users need to pause and resume progress.
    • Persisting intermediate state on the server allows users to start an application on mobile, save progress, and finish later on desktop without losing data.
  • Creation of final domain entities requires atomic, all-or-nothing execution.
    • Downstream domain entities (e.g., active bank accounts or underwriting cases) should not exist in invalid or partially configured states.
  • Multiple users or systems contribute data to the same process.
    • e.g., A primary borrower fills out personal information, and an employer or co-borrower uploads income verification to the same draft instance later.

22.3. When NOT to Use Draft Workflow Resource Pattern

Avoid Draft Workflow Resource when:

  • The resource can be created in a single HTTP request.
    • If all required fields are known upfront, use standard POST /resources CRUD creation.
  • The workflow consists of independent, immediate CRUD resources.
    • If adding a user or uploading an asset during a wizard has immediate independent utility regardless of whether the wizard completes, create those resources directly.
  • The process is strictly linear and ephemeral.
    • If steps occur in a single short-lived front-end session and no multi-device persistence is required, maintain state in client application memory until a final submission call.

22.4. What the Pattern Looks Like

Below are detailed HTTP interaction flows demonstrating a Commercial Loan / Mortgage Application wizard.


1. Initiate Loan Application Draft

The client creates a draft application session.

Request

POST /loan-applications HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "applicantId": "usr_99182",
  "productType": "COMMERCIAL_MORTGAGE"
}

Response

HTTP/1.1 201 Created
Location: https://api.example.com/loan-applications/app_draft_4012
Content-Type: application/json

{
  "id": "app_draft_4012",
  "applicantId": "usr_99182",
  "productType": "COMMERCIAL_MORTGAGE",
  "status": "DRAFT",
  "currentStep": "BORROWER_DETAILS",
  "completionPercentage": 10,
  "createdAt": "2026-08-04T10:00:00Z"
}

2. Step 1: Add Borrower & Employment Details

The client submits Step 1 information via PATCH.

Request

PATCH /loan-applications/app_draft_4012 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "borrower": {
    "annualIncome": 185000,
    "employerName": "Apex Technologies LLC",
    "yearsEmployed": 5
  }
}

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "app_draft_4012",
  "status": "DRAFT",
  "currentStep": "PROPERTY_INFO",
  "completionPercentage": 40,
  "borrower": {
    "annualIncome": 185000,
    "employerName": "Apex Technologies LLC",
    "yearsEmployed": 5
  }
}

3. Step 2: Add Property & Appraisal Details

The client submits Step 2 details.

Request

PATCH /loan-applications/app_draft_4012 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "property": {
    "address": "742 Evergreen Terrace, Springfield",
    "estimatedValue": 750000,
    "propertyType": "COMMERCIAL_RETAIL"
  },
  "requestedLoanAmount": 500000
}

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "app_draft_4012",
  "status": "DRAFT",
  "currentStep": "REVIEW_AND_SUBMIT",
  "completionPercentage": 100,
  "missingRequiredFields": []
}

4. Final Action: Submit Application (POST /loan-applications/{id}/submit)

The client triggers submission. The API validates the draft, transitions status to SUBMITTED, and provisions the real underwriting entities.

Note: This example uses the Functional Resource pattern for the submit action.

Request

POST /loan-applications/app_draft_4012/submit HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "app_draft_4012",
  "status": "SUBMITTED",
  "underwritingCaseId": "case_88391",
  "submittedAt": "2026-08-04T10:45:00Z",
  "message": "Loan application successfully submitted for underwriting."
}

22.5. Anti-Patterns to Avoid

1. Creating production entities prematurely in draft states

  • Instantiating actual Account, Loan, or BillingSubscription records with placeholder or isTemporary=true flags during early wizard steps pollutes production databases and makes reporting, auditing, and cleanup extremely difficult. Hold all temporary data in the workflow draft resource until submission.

2. Over-validating during intermediate draft updates

  • Rejecting a PATCH /loan-applications/{id} request because step 3 fields are missing while the user is still on step 1 renders the draft pattern useless. Relax validation rules on intermediate draft updates, enforcing strict full-payload validation only when the final /submit action is invoked. Alternatively, use a validation library that performs progressive validation of the fields provided after each step is completed.

3. Lack of draft expiration / TTL cleanup policies

  • Allowing abandoned drafts to linger indefinitely clutters storage. Implement automatic cleanup or archiving for inactive drafts (e.g., auto-expire drafts after 30 days of inactivity).

4. Using POST collection calls for each wizard step

  • Creating separate endpoints like POST /loan-step-1, POST /loan-step-2, and POST /loan-step-3 fragments state into artificial endpoints. Use a single draft resource (/loan-applications/{id}) and update it using HTTP PATCH. This decouples the front-end user interface from the back-end storage, allowing the user interface to evolve over time without requiring corresponding changes to the API.

22.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating the draft workflow lifecycle: creation, progressive partial updates, draft retrieval, and final submission.

openapi: 3.0.3
info:
  title: Commercial Loan Application API - Draft Workflow Example
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /loan-applications:
    post:
      summary: Initiate a new loan application draft
      description: Creates a temporary workflow draft session to accumulate application data across multiple steps.
      tags: [Loan Applications]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - applicantId
                - productType
              properties:
                applicantId:
                  type: string
                  example: usr_99182
                productType:
                  type: string
                  example: COMMERCIAL_MORTGAGE
      responses:
        '201':
          description: Draft application created
          headers:
            Location:
              schema:
                type: string
                example: "https://api.example.com/loan-applications/app_draft_4012"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanApplicationDraft'

  /loan-applications/{applicationId}:
    get:
      summary: Retrieve loan application draft
      description: Fetches current progress, stored draft fields, and completion details.
      tags: [Loan Applications]
      parameters:
        - in: path
          name: applicationId
          required: true
          schema:
            type: string
            example: app_draft_4012
      responses:
        '200':
          description: Draft details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanApplicationDraft'

    patch:
      summary: Update draft application step data
      description: Progressively accumulates borrower, property, or loan term data into the draft.
      tags: [Loan Applications]
      parameters:
        - in: path
          name: applicationId
          required: true
          schema:
            type: string
            example: app_draft_4012
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoanApplicationUpdate'
      responses:
        '200':
          description: Draft updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoanApplicationDraft'

  /loan-applications/{applicationId}/submit:
    post:
      summary: Submit completed loan application
      description: Validates all required draft fields, transitions status to SUBMITTED, and creates underwriting case.
      tags: [Loan Applications]
      parameters:
        - in: path
          name: applicationId
          required: true
          schema:
            type: string
            example: app_draft_4012
      responses:
        '200':
          description: Application submitted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmissionResult'
        '422':
          description: Unprocessable Entity - Draft is incomplete or failed validation rules
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

components:
  schemas:
    LoanApplicationDraft:
      type: object
      properties:
        id:
          type: string
          example: app_draft_4012
        applicantId:
          type: string
          example: usr_99182
        productType:
          type: string
          example: COMMERCIAL_MORTGAGE
        status:
          type: string
          enum: [DRAFT, SUBMITTED, EXPIRED]
          example: DRAFT
        currentStep:
          type: string
          example: BORROWER_DETAILS
        completionPercentage:
          type: integer
          example: 40
        borrower:
          type: object
          properties:
            annualIncome:
              type: number
              example: 185000
            employerName:
              type: string
              example: Apex Technologies LLC
            yearsEmployed:
              type: integer
              example: 5
        property:
          type: object
          properties:
            address:
              type: string
              example: "742 Evergreen Terrace"
            estimatedValue:
              type: number
              example: 750000
            propertyType:
              type: string
              example: COMMERCIAL_RETAIL
        requestedLoanAmount:
          type: number
          example: 500000
        createdAt:
          type: string
          format: date-time

    LoanApplicationUpdate:
      type: object
      properties:
        borrower:
          type: object
        property:
          type: object
        requestedLoanAmount:
          type: number

    SubmissionResult:
      type: object
      properties:
        id:
          type: string
          example: app_draft_4012
        status:
          type: string
          example: SUBMITTED
        underwritingCaseId:
          type: string
          example: case_88391
        submittedAt:
          type: string
          format: date-time
        message:
          type: string
          example: "Loan application successfully submitted for underwriting."

    ProblemDetails:
      type: object
      properties:
        type:
          type: string
          example: "https://api.example.com/errors/incomplete-draft"
        title:
          type: string
          example: "Unprocessable Entity"
        status:
          type: integer
          example: 422
        detail:
          type: string
          example: "Property details are required before submitting the loan application."

22.7. Visualizing Draft Workflow Resource Pattern (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor Client as Client App (Wizard UI)
    participant API as API Server
    participant DraftDB as Workflow Draft Store
    participant CoreDomain as Underwriting Core Engine

    Note over Client,CoreDomain: Step 1: Initiate Workflow Draft
    Client->>API: POST /loan-applications { applicantId, productType }
    API->>DraftDB: Save initial draft record (status: DRAFT)
    DraftDB-->>API: Created app_draft_4012
    API-->>Client: 201 Created { id: "app_draft_4012", completionPercentage: 10 }

    Note over Client,CoreDomain: Step 2: Progressive Accumulation (Borrower & Property Info)
    Client->>API: PATCH /loan-applications/app_draft_4012 { borrower: { income: 185000 } }
    API->>DraftDB: Update draft payload & calculate completion %
    API-->>Client: 200 OK { completionPercentage: 40 }

    Client->>API: PATCH /loan-applications/app_draft_4012 { property: { address: "..." } }
    API->>DraftDB: Update draft payload
    API-->>Client: 200 OK { completionPercentage: 100 }

    Note over Client,CoreDomain: Step 3: Final Completion & Provisioning
    Client->>API: POST /loan-applications/app_draft_4012/submit
    API->>DraftDB: Retrieve full draft payload
    API->>API: Validate complete payload against business rules
    
    alt Validation Failed
        API-->>Client: 422 Unprocessable Entity { missingRequiredFields: [...] }
    else Validation Passed
        API->>CoreDomain: Create UnderwritingCase & Loan records
        CoreDomain-->>API: Success (case_88391)
        API->>DraftDB: Update draft status to SUBMITTED
        API-->>Client: 200 OK { status: "SUBMITTED", underwritingCaseId: "case_88391" }
    end