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)¶
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:
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¶
✅ 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:
- Agregar el nuevo nombre (additive).
- Mantener ambos por 12+ meses.
- Documentar como "preferred name".
- Bumpear a v2 cuando hagas el cleanup.
❌ 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¶
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:
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:
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/v2si acumulamos breaks.
Referencias¶
- analysis/rest-api-design — REST endpoints
- analysis/worker-sdk-go-design — SDK
- analysis/migration-from-camunda-8 — migración cross-versions
- Stripe API versioning — referencia industry
- GitHub API versioning
- Sunset HTTP header (RFC 8594)
- Deprecation HTTP header (draft)