Saltar a contenido

API versioning strategy

Cómo evolucionar la REST API, los SDKs, el BPMN parser y el wire protocol del engine sin romper clientes existentes. Cubre semver, deprecation, sunset, compatibility windows.

Cinco surfaces a versionar (separados)

Surface Versionado Compat window
REST API URL path /api/v1/... + headers 24 meses
Go SDK semver del módulo Go dos majors
TypeScript SDK semver npm dos majors
BPMN schema (Zeebe extensions) XML namespace versioning 36 meses
Wire protocol (engine ↔ workers) Header Wf-Protocol-Version 12 meses

Cada surface tiene cadencia distinta. La REST API rara vez bumpa MAJOR; el SDK puede ser más agresivo.

REST API

URL path versioning (defacto)

/api/v1/instances
/api/v1/jobs
/api/v2/instances  ← cuando bumpa MAJOR

Razones para preferir path-based sobre header-based: - Visible en logs / dashboards. - Cacheable por proxies. - Trivial routing en gateway. - Discoverable (curl + brain).

Header-based queda para "modifiers" no version:

Accept: application/json
Accept-Encoding: gzip
Wf-Trace-Id: abc-123

Semver para REST

  • MAJOR: breaking change (rename field, type change, remove endpoint).
  • MINOR: additive (new optional field, new endpoint).
  • PATCH: bug fixes, perf, docs.

Solo MAJOR cambia el path. MINOR y PATCH son backwards compatible.

Lifecycle de un endpoint

stateDiagram-v2
    [*] --> ACTIVE
    ACTIVE --> DEPRECATED: warning header
    DEPRECATED --> SUNSET: 404 / 410 Gone
    SUNSET --> [*]

Deprecation: minimum 12 months from announcement.

GET /api/v1/instances/123 HTTP/1.1


HTTP/1.1 200 OK
Deprecation: Sat, 14 May 2027 00:00:00 GMT
Sunset: Mon, 14 May 2028 00:00:00 GMT
Link: </api/v2/instances/123>; rel="successor-version"

{...}

Patrones de evolución sin major

✅ Add optional field

// v1 antes
{ "key": 123, "status": "ACTIVE" }

// v1 después
{ "key": 123, "status": "ACTIVE", "priority": 50 }  // nuevo, ignorable

✅ Add new endpoint

POST /api/v1/instances        (existente)
POST /api/v1/instances/batch  (nuevo)

✅ Accept additional input formats

// v1 al principio: solo "amount"
{ "amount": 99.99 }

// v1 después: acepta "amount" o "amountCents"
{ "amountCents": 9999 }

Validation: si ambos, error 400 con explicación clara.

⚠️ Rename field — requiere coordinación

NO permitido en v1. En su lugar:

  1. Agregar el nuevo nombre (additive).
  2. Mantener ambos por 12+ meses.
  3. Documentar como "preferred name".
  4. Bumpear a v2 cuando hagas el cleanup.
// v1 transición
{ "instanceKey": 123, "key": 123 }  // duplicado intencional

❌ Breaking change — solo en v2+

Cualquier de estos requiere MAJOR: - Remove field. - Change type (string → number). - Change semantics (e.g., "completed" pasa a incluir "cancelled"). - Add required field. - Change auth requirements.

v1 vs v2 coexistence

Engine versión 2.0 expone:
  /api/v1/...  ← legacy, en deprecation
  /api/v2/...  ← current

Engine versión 3.0:
  /api/v1/...  ← sunset (return 410)
  /api/v2/...  ← legacy, deprecated
  /api/v3/...  ← current

Compatibility window: 24 meses entre primer release de N+1 y sunset de N.

Discovery API

GET /api/versions

HTTP/1.1 200 OK
{
  "current": "v2",
  "supported": ["v1", "v2"],
  "deprecated": ["v1"],
  "sunset": {
    "v1": "2028-05-14"
  },
  "links": {
    "v1": "/api/v1",
    "v2": "/api/v2"
  }
}

Cliente smart usa esto para warning automático.

Go SDK versioning

Module path con MAJOR

// v1
import "github.com/example/wf-sdk-go/wfclient"

// v2 — path bumpea
import "github.com/example/wf-sdk-go/v2/wfclient"

Permite usar ambos simultáneamente (durante migración del usuario).

import (
    legacy "github.com/example/wf-sdk-go/wfclient"
    v2 "github.com/example/wf-sdk-go/v2/wfclient"
)

// Worker viejo
legacyClient, _ := legacy.New(...)

// Worker nuevo
newClient, _ := v2.New(...)

Útil para migration progresiva en monorepo con muchos workers.

Cadencia recomendada

  • MAJOR: cada 18-24 meses (cuando se acumulan suficientes breaks útiles).
  • MINOR: mensual o por necesidad.
  • PATCH: weekly OK si hay bugs.

Deprecation en el SDK

// Deprecated: use NewWorker instead. Will be removed in v3.
func RegisterWorker(opts WorkerOptions, h JobHandler) error {
    // ...
}

// NewWorker is the preferred API.
func NewWorker(opts WorkerOptions, h JobHandler) (*Worker, error) {
    // ...
}

go vet detecta uso de deprecated; warning en IDE.

Compatibility matrix

Documentar:

| SDK version | Engine REST API | Status |
|---|---|---|
| Go SDK v1.x | /api/v1 only | EOL 2028-05-14 |
| Go SDK v2.x | /api/v1 + /api/v2 | Maintained |
| Go SDK v3.x | /api/v2 + /api/v3 | Current |

Cliente que use Go SDK v3 con engine v1.0 (solo /api/v1) → error claro: "SDK v3 requires engine v2.0+".

Breaking changes acumuladas en MAJOR

NO hacer 5 v2 → v3 → v4 → v5 → v6 en 6 meses. Acumular:

v2 → v3 changelog:
  - Renamed JobHandler signature (added context as first arg)
  - Removed deprecated RegisterWorker
  - Changed default retry policy
  - New required field in WorkerOptions

Una migración mayor cada 1.5-2 años es soportable; semestral no lo es.

BPMN schema versioning

XML namespaces

<bpmn:definitions
    xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
    ...>
  <bpmn:serviceTask>
    <bpmn:extensionElements>
      <zeebe:taskDefinition type="charge-payment"/>
    </bpmn:extensionElements>
  </bpmn:serviceTask>
</bpmn:definitions>

Cuando agregamos un campo nuevo:

<!-- v1.0 -->
<zeebe:taskDefinition type="charge-payment"/>

<!-- v1.1: agregado retries opcional, MISMO namespace -->
<zeebe:taskDefinition type="charge-payment" retries="5"/>

<!-- v2.0: cambio breaking (rename type → name), nuevo namespace -->
<zeebe2:taskDefinition name="charge-payment"/>

Engine parser soporta múltiples namespaces simultáneamente.

Validation lifecycle

  • Deploy con namespace v1: ✅ M1-M4.
  • Deploy con namespace v2: ✅ M2+.
  • Deploy con namespace v1 en M5: ⚠️ warning "schema deprecated, migrate to v2 by 2028-12".
  • Sunset v1 en M6: ❌ deploy rejected.

Migration tooling

wf process migrate-schema --from-version 1.0 --to-version 2.0 order-flow.bpmn

Tool re-escribe el XML aplicando reglas conocidas (renames, additions).

Wire protocol (engine internal)

Si tenemos cluster multi-node, los nodos hablan entre sí. Wire protocol versionado:

Header: Wf-Protocol-Version: 2
Body: <command-data>

Reglas: - Nodo viejo (v1) habla con nodo nuevo (v2): nodo nuevo soporta v1. - Nodo nuevo (v2) habla con nodo viejo (v1): nodo nuevo downgrade su request a v1. - Cluster mixto OK durante upgrade rolling.

Drop wire protocol N → N+1: solo cuando todos los nodos están en N+1. Otherwise split-brain risk.

Worker streaming protocol

Workers ↔ engine via long-polling:

GET /api/v1/jobs/stream?type=charge-payment
  Header: Wf-Protocol-Version: 2
  Header: Wf-Worker-Capability: streaming-v2

→ Engine responde con jobs en formato compatible con worker

Si worker tiene capability streaming-v2 pero engine solo soporta streaming-v1: downgrade automático.

Database schema (interno)

Schema de Postgres también versionado:

CREATE TABLE schema_version (
    version INTEGER PRIMARY KEY,
    applied_at TIMESTAMPTZ NOT NULL,
    migration_name TEXT NOT NULL
);

Engine al startup: 1. Lee schema_version. 2. Si < app_required_version: aborta, "DB schema outdated, run migrations first". 3. Si > app_max_supported: aborta, "DB schema newer than this engine version".

Garantiza coherencia. Migraciones se aplican vía operator o wf db migrate.

Changelogs disciplina

Cada release publica:

# v2.3.0 - 2026-05-14

## ⚡ Breaking changes
(none in MINOR)

## ✨ Added
- REST: POST /api/v2/instances/batch
- Go SDK: WorkerOptions.MaxBackoff field
- BPMN: zeebe:retryBackoff in taskDefinition

## 🔧 Changed
- Default request timeout increased 30s → 60s

## 🐛 Fixed
- Race condition in timer scheduler under load

## 🗑️ Deprecated
- Go SDK: RegisterWorker (use NewWorker)
- REST: GET /api/v1/instances/list (use /api/v1/instances)

Conventional Commits → CHANGELOG generation automation.

Tooling for clients

SDK helpers

client, err := wfclient.New(url,
    wfclient.WithRequiredEngineVersion("2.0.0"),  // fail fast si engine es viejo
)

CLI version check

wf version
# CLI:    v1.2.3
# Engine: v2.1.0
# Compatibility: ✅ OK
# Latest: v1.3.0 available (https://...)

Auto-update opcional:

wf upgrade

CI check

- name: Verify API compatibility
  run: |
    wf api check --required-version "v1.x"
    # Falla si el engine apuntado no soporta v1

Edge cases

Cliente con SDK super viejo

Si SDK v0.5 (de antes de v1.0): - Engine devuelve 426 Upgrade Required. - Body explica versiones soportadas. - Client falla early con mensaje claro.

Mixed-version cluster during upgrade

Engine N+1 leader, engine N follower:
  - Leader procesa comandos en formato N+1.
  - Follower lee command log con parser N+1 (debe soportar N para replay).

Parser invariant: parser de versión N debe deserializar comandos de versiones N-1, N-2 (back-compat window).

Old BPMN, new feature

Proceso desplegado en M2 (BPMN v1.0).
M5 introduces new feature en BPMN v2.0.
El proceso v1.0 sigue funcionando — no se actualiza automágicamente.

Para usar feature nueva, redeploy con BPMN v2.0.

Anti-patterns documentados

❌ "Versionless API"

GraphQL-style "siempre back-compat" en teoría, pero termina en deprecation hell con campos legacy nunca removidos.

❌ Date-based versioning

/api/2026-05-14/instances parece nice pero acumula muchas versiones, difícil discovery.

❌ Versioning por query param

/api/instances?version=2 → mal cacheable, mal routable.

❌ Forzar upgrade sin grace period

"Upgrade now or we cut you off in 2 weeks" → daña la relación con users.

Roadmap

  • M1: Solo /api/v1. No deprecations todavía.
  • M2: Discovery endpoint /api/versions. CHANGELOG disciplinado.
  • M3: SDK compatibility check al startup.
  • M5+ (después de 18m): Posible bumpear a /api/v2 si acumulamos breaks.

Referencias