29. Pattern: Change Request Workflow

The Change Request Workflow pattern replaces the traditional direct-mutation CRUD model (PUT, PATCH) with a formalized proposal and approval lifecycle. Instead of immediately modifying a resource, clients submit a strongly-typed “change request” specifying the exact kind of modification they wish to make.

By utilizing a polymorphic discriminator (e.g., changeType), the API enforces distinct validation schemas for different types of changes. The proposed modification remains isolated in a pending state until an authorized entity reviews and approves it, at which point the system applies the changes to the underlying resource.

29.1. Overview

In standard CRUD REST APIs, issuing a PATCH /accounts/123 immediately updates the account. While efficient, this direct-mutation approach breaks down in regulated environments, enterprise systems, or scenarios requiring “maker-checker” (four-eyes) principles.

If a user wants to change their legal address or increase their credit limit, these actions often require human review, automated fraud checks, or strict audit logging. If you try to force this into a standard PATCH request, you end up with messy, implicit state machines (e.g., the PATCH succeeds, but the account is secretly locked pending review).

The Change Request Workflow solves this by treating the intent to change as a first-class resource:

  • The client submits a POST to a nested collection (e.g., /accounts/123/change-requests).
  • The change request includes a discriminator (e.g., changeType: "UpdateAddress" or changeType: "IncreaseCreditLimit").
  • The API uses the discriminator to validate the request against a specific schema tailored exactly to that modification.
  • A reviewer (or automated system) evaluates the change request and issues an approval or rejection.
  • Upon approval, the backend safely applies the mutation to the primary resource.

29.2. When to Use Change Request Workflow

Use the Change Request Workflow when:

  • Enforcing “Maker-Checker” or Dual Control policies.
    • A clerk proposes a change, and a manager must approve it before it takes effect (common in banking, HR, and access management).
  • Modifications require asynchronous validation.
    • If a change requires a background process (e.g., KYC/AML verification) that takes hours or days, a synchronous PATCH is impossible.
  • Different modifications require vastly different validation rules.
    • Changing a username requires checking uniqueness; changing a banking routing number requires algorithmic checksum validation. A polymorphic discriminator handles these cleanly.
  • Strict audit trails are mandated.
    • Regulators need to see not just what changed, but who proposed it, why, and who authorized it.

29.3. When NOT to Use Change Request Workflow

Avoid the Change Request Workflow when:

  • The user is the sole owner of low-stakes data.
    • Updating a profile biography, tweaking UI preferences, or renaming a personal playlist should use direct PATCH or PUT.
  • The system requires real-time collaborative editing.
    • Workflows requiring operational transformation or WebSockets (like editing a shared document) clash with rigid approval lifecycles.
  • The approval process is completely automatic and instantaneous.
    • If the server can validate and approve the change in milliseconds without external dependency, exposing the change request lifecycle to the client adds unnecessary friction.

29.4. What the Pattern Looks Like

Below are HTTP request and response flows demonstrating the submission of a polymorphic change request and its subsequent approval.

1. Submitting the Change Request (The “Maker”)

The client proposes a modification to an account. By specifying changeType: "CreditLimitIncrease", the API knows to validate the changeRequest against the credit limit schema.

Request

POST /accounts/acc_881/change-requests HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
  "changeType": "CreditLimitIncrease",
  "reason": "Customer requested standard yearly increase.",
  "changeRequest": {
    "requestedLimit": 15000.00,
    "currency": "USD"
  }
}

Response

The server validates the specific schema, creates the change request in a pending state, and returns it. The core account resource remains untouched.

HTTP/1.1 201 Created
Location: https://api.example.com/accounts/acc_881/change-requests/cr_992
Content-Type: application/json

{
  "id": "cr_992",
  "accountId": "acc_881",
  "status": "pending",
  "changeType": "CreditLimitIncrease",
  "submittedBy": "usr_maker_01",
  "submittedAt": "2026-09-25T14:38:23Z",
  "changeRequest": {
    "requestedLimit": 15000.00,
    "currency": "USD"
  },
  "links": {
    "approve": "/accounts/acc_881/change-requests/cr_992/approve",
    "reject": "/accounts/acc_881/change-requests/cr_992/reject"
  }
}

2. Approving the Change Request (The “Checker”)

An authorized manager or automated system acts on the pending request.

Request

POST /accounts/acc_881/change-requests/cr_992/approve HTTP/1.1
Host: api.example.com
Authorization: Bearer manager_token...
Content-Type: application/json

{
  "comments": "Approved based on account history."
}

Response

The system marks the request as approved and implicitly applies the 15000.00 limit to the parent acc_881 resource.

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

{
  "id": "cr_992",
  "status": "approved",
  "resolvedBy": "usr_checker_99",
  "resolvedAt": "2026-09-25T15:10:00Z"
}

29.5. Anti-Patterns to Avoid

  • Using a generic changes blob instead of a Discriminator:
    • Allowing users to submit {"fieldsToChange": {"limit": 15000, "status": "active"}} bypasses strict schema validation. You cannot easily enforce that a user providing a new address also provides a valid zip code. Use a changeType discriminator mapped to concrete, enforceable schemas.
  • Overloading PATCH with implicit workflows:
    • Returning a 202 Accepted from a PATCH request and silently creating a workflow task in the background hides the domain model from the client. The client has no URI to check the status of their pending change.
  • Applying changes before approval:
    • Mutating the primary resource into a “locked” or “provisional” state breaks read consistency for other clients fetching the resource. The primary resource must reflect the last known approved state until the change request clears.

29.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating the polymorphic ChangeRequest utilizing oneOf and a discriminator.

openapi: 3.0.3
info:
  title: Account Management API - Change Request Workflow
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /accounts/{accountId}/change-requests:
    post:
      summary: Propose a modification to an account
      description: Submits a change request for review. Uses polymorphic schema validation based on changeType.
      tags: [Change Requests]
      parameters:
        - in: path
          name: accountId
          required: true
          schema:
            type: string
            example: "acc_881"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangeRequestSubmit'
      responses:
        '201':
          description: Change Request created and pending review.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangeRequestResponse'

  /accounts/{accountId}/change-requests/{requestId}/approve:
    post:
      summary: Approve a change request
      description: Approves a pending change request and applies the mutation to the parent account.
      tags: [Change Requests]
      parameters:
        - in: path
          name: accountId
          required: true
          schema:
            type: string
        - in: path
          name: requestId
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                comments:
                  type: string
      responses:
        '200':
          description: Successfully approved.

components:
  schemas:
    ChangeRequestSubmit:
      type: object
      required:
        - changeType
        - changeRequest
      discriminator:
        propertyName: changeType
        mapping:
          UpdateAddress: '#/components/schemas/UpdateAddressChangeRequest'
          CreditLimitIncrease: '#/components/schemas/CreditLimitIncreaseChangeRequest'
      properties:
        changeType:
          type: string
        reason:
          type: string
        changeRequest:
          # In OpenAPI 3, the discriminator guides validation against the referenced schema
          type: object 

    UpdateAddressChangeRequest:
      type: object
      required: [street, city, zipCode]
      properties:
        street:
          type: string
        city:
          type: string
        zipCode:
          type: string

    CreditLimitIncreaseChangeRequest:
      type: object
      required: [requestedLimit]
      properties:
        requestedLimit:
          type: number
        currency:
          type: string
          default: "USD"

    ChangeRequestResponse:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum: [pending, approved, rejected]
        changeType:
          type: string
        submittedBy:
          type: string
        changeRequest:
          type: object

29.7. Visualizing Change Request Workflow (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor Maker as User (Maker)
    actor Checker as Admin (Checker)
    participant API as API Gateway
    participant CR_DB as Change Request DB
    participant Core_DB as Core Account DB

    Note over Maker,Core_DB: Phase 1: Proposal (Validation & Storage)
    Maker->>API: POST /change-requests (Type: CreditLimitIncrease)
    API->>API: Validate change request details against CreditLimit schema
    API->>CR_DB: Insert Status='pending'
    API-->>Maker: 201 Created (Pending)

    Note over Checker,Core_DB: Phase 2: Review & Execution
    Checker->>API: GET /change-requests?status=pending
    API-->>Checker: Returns pending requests
    Checker->>API: POST /change-requests/cr_992/approve
    
    API->>CR_DB: Update Status='approved'
    API->>Core_DB: Apply requested mutation to Account resource
    Core_DB-->>API: Success
    
    API-->>Checker: 200 OK (Approved)