The Functional Resource pattern exposes command-style, compute-style, or decision-style operations that do not represent a resource lifecycle.
These operations take an input, perform work, and return an output, without implying CRUD behavior or lifecycle transitions.
6.1. Overview
Functional Resources:
- Represent business operations, not resources
- Are often pure functions (
input → output) - Do not create long-lived entities
- Do not require CRUD semantics or state transitions
- Are named using verb–noun or action-oriented terms
This pattern is ideal for calculations, simulations, estimates, and decisioning operations.
These endpoints are named using a verb–noun structure such as:
POST /estimate-taxPOST /calculate-shippingPOST /check-eligibilityPOST /compute-loan-schedule
6.2. When to Use the Functional Resource Pattern
Use this pattern when the API is:
-
Performing a calculation
POST /estimate-tax
-
Generating a quote or estimate (nondurable)
POST /calculate-shipping
-
Checking conditions or eligibility
POST /check-eligibility
-
Computing financial schedules
POST /compute-loan-schedule
-
Combining multiple services to produce a derived value
These operations:
- Are not CRUD
- Don’t create or store a persistent object
- Don’t move a resource through a lifecycle (which would be Extended CRUD)
6.3. When NOT to Use This Pattern
Avoid it when:
-
The result will become a first-class resource
- e.g., a “quote” that needs to be retrieved, updated, and accepted
-
A resource has a real lifecycle
- e.g., a document moving through Draft → Submitted → Approved
-
The domain action represents a state transition
- e.g., canceling an order → Extended CRUD
-
You are trying to avoid designing a real resource by hiding it behind a function
Decision rule: Use Functional Resource for commands without lifecycle. Use Extended CRUD for lifecycles with transitions.
6.4. What the Pattern Looks Like
A typical Functional Resource endpoint:
POST /estimate-tax
POST /calculate-shipping
POST /compute-interest
POST /check-eligibility
Characteristics:
- Expressed as a top-level command
- No resource ID in the path
- Self-contained request + response
- Stateless from the client’s perspective
- No CRUD or lifecycle implied
6.5. Anti-Patterns to Avoid
❌ 1. Modeling compute operations as CRUD
POST /tax-estimates
GET /tax-estimates/{id}
This adds lifecycle that does not exist.
❌ 2. Embedding compute triggers in PATCH requests
PATCH /orders/{id} { "recalculateTax": true }
Confusing and hides behavior.
❌ 3. Using Extended CRUD naming for commands
POST /orders/{id}/estimate-tax
This incorrectly suggests tax estimation is part of the Order lifecycle.
✅ Correct form:
POST /estimate-tax
This communicates a pure computation.
6.6. OpenAPI Example
openapi: 3.0.3
info:
title: Tax Estimation API - Functional Resource Pattern Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/estimate-tax:
post:
summary: Estimate sales tax for a set of items
tags: [Tax]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TaxEstimateRequest'
examples:
default:
value:
currency: "USD"
destination:
country: "US"
postalCode: "94103"
region: "CA"
items:
- sku: "BOOK-DDD-001"
quantity: 1
unitPrice: 5500
taxCategory: "BOOK"
responses:
'200':
description: Tax estimated
content:
application/json:
schema:
$ref: '#/components/schemas/TaxEstimateResult'
examples:
default:
value:
currency: "USD"
totalTaxAmount: 950
items:
- sku: "BOOK-DDD-001"
quantity: 1
lineTaxAmount: 400
- sku: "BOOK-API-001"
quantity: 2
lineTaxAmount: 550
components:
schemas:
TaxEstimateRequest:
type: object
properties:
currency:
type: string
example: "USD"
destination:
type: object
properties:
country:
type: string
example: "US"
postalCode:
type: string
example: "94103"
region:
type: string
example: "CA"
required: [country, postalCode]
items:
type: array
items:
$ref: '#/components/schemas/TaxEstimateItem'
required: [currency, destination, items]
TaxEstimateItem:
type: object
properties:
sku:
type: string
example: "BOOK-DDD-001"
quantity:
type: integer
example: 1
unitPrice:
type: integer
example: 5500
taxCategory:
type: string
example: "BOOK"
required: [sku, quantity, unitPrice]
TaxEstimateResult:
type: object
properties:
currency:
type: string
example: "USD"
totalTaxAmount:
type: integer
example: 950
items:
type: array
items:
$ref: '#/components/schemas/TaxEstimateLineResult'
required: [currency, totalTaxAmount, items]
TaxEstimateLineResult:
type: object
properties:
sku:
type: string
example: "BOOK-DDD-001"
quantity:
type: integer
example: 1
lineTaxAmount:
type: integer
example: 400
required: [sku, quantity, lineTaxAmount]
6.7. Visualizing Functional Resources (Mermaid Diagram)
sequenceDiagram
autonumber
participant C as Client
participant API as Tax API
participant RTS as Tax Rate Service
participant GEO as Geo Lookup Service
C->>API: POST /estimate-tax<br/>{ destination, items }
API->>GEO: Resolve region for postalCode
GEO-->>API: regionCode = "CA-SF"
API->>RTS: Calculate rates for regionCode + items
RTS-->>API: per-item tax amounts
API-->>C: 200 OK<br/>{ TaxEstimateResult }
This clearly shows:
- No lifecycle
- No resource ID
- A single compute-style command