@zigrivers/scaffold 3.15.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -12
- package/content/knowledge/backend/backend-fintech-broker-integration.md +244 -0
- package/content/knowledge/backend/backend-fintech-compliance.md +181 -0
- package/content/knowledge/backend/backend-fintech-data-modeling.md +210 -0
- package/content/knowledge/backend/backend-fintech-ledger.md +226 -0
- package/content/knowledge/backend/backend-fintech-observability.md +151 -0
- package/content/knowledge/backend/backend-fintech-order-lifecycle.md +213 -0
- package/content/knowledge/backend/backend-fintech-risk-management.md +150 -0
- package/content/knowledge/backend/backend-fintech-testing.md +197 -0
- package/content/knowledge/core/automated-review-tooling.md +31 -26
- package/content/knowledge/core/multi-model-review-dispatch.md +30 -55
- package/content/knowledge/core/multi-service-api-contracts.md +634 -0
- package/content/knowledge/core/multi-service-architecture.md +492 -0
- package/content/knowledge/core/multi-service-auth.md +706 -0
- package/content/knowledge/core/multi-service-data-ownership.md +539 -0
- package/content/knowledge/core/multi-service-observability.md +545 -0
- package/content/knowledge/core/multi-service-resilience.md +710 -0
- package/content/knowledge/core/multi-service-task-decomposition.md +615 -0
- package/content/knowledge/core/multi-service-testing.md +728 -0
- package/content/methodology/backend-fintech.yml +46 -0
- package/content/methodology/custom-defaults.yml +6 -0
- package/content/methodology/deep.yml +6 -0
- package/content/methodology/multi-service-overlay.yml +103 -0
- package/content/methodology/mvp.yml +6 -0
- package/content/pipeline/architecture/service-ownership-map.md +83 -0
- package/content/pipeline/quality/cross-service-auth.md +96 -0
- package/content/pipeline/quality/cross-service-observability.md +104 -0
- package/content/pipeline/quality/integration-test-plan.md +106 -0
- package/content/pipeline/specification/inter-service-contracts.md +95 -0
- package/content/tools/post-implementation-review.md +36 -7
- package/content/tools/review-code.md +33 -8
- package/content/tools/review-pr.md +79 -95
- package/dist/cli/commands/adopt.cli-flags.test.js +20 -0
- package/dist/cli/commands/adopt.cli-flags.test.js.map +1 -1
- package/dist/cli/commands/adopt.d.ts.map +1 -1
- package/dist/cli/commands/adopt.js +11 -3
- package/dist/cli/commands/adopt.js.map +1 -1
- package/dist/cli/commands/complete.d.ts +1 -0
- package/dist/cli/commands/complete.d.ts.map +1 -1
- package/dist/cli/commands/complete.js +26 -8
- package/dist/cli/commands/complete.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts +1 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +19 -6
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/decisions.d.ts +1 -0
- package/dist/cli/commands/decisions.d.ts.map +1 -1
- package/dist/cli/commands/decisions.js +18 -4
- package/dist/cli/commands/decisions.js.map +1 -1
- package/dist/cli/commands/info.d.ts +1 -0
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +25 -3
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init-from.test.d.ts +2 -0
- package/dist/cli/commands/init-from.test.d.ts.map +1 -0
- package/dist/cli/commands/init-from.test.js +315 -0
- package/dist/cli/commands/init-from.test.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +239 -129
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +20 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/next.d.ts +1 -0
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +40 -4
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/next.test.js +151 -0
- package/dist/cli/commands/next.test.js.map +1 -1
- package/dist/cli/commands/reset.d.ts +1 -0
- package/dist/cli/commands/reset.d.ts.map +1 -1
- package/dist/cli/commands/reset.js +77 -29
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/rework.d.ts +1 -0
- package/dist/cli/commands/rework.d.ts.map +1 -1
- package/dist/cli/commands/rework.js +16 -2
- package/dist/cli/commands/rework.js.map +1 -1
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +65 -13
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/run.test.js +192 -3
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.d.ts +1 -0
- package/dist/cli/commands/skip.d.ts.map +1 -1
- package/dist/cli/commands/skip.js +24 -7
- package/dist/cli/commands/skip.js.map +1 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +51 -4
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/status.test.js +128 -0
- package/dist/cli/commands/status.test.js.map +1 -1
- package/dist/cli/guards-coverage.test.d.ts +2 -0
- package/dist/cli/guards-coverage.test.d.ts.map +1 -0
- package/dist/cli/guards-coverage.test.js +26 -0
- package/dist/cli/guards-coverage.test.js.map +1 -0
- package/dist/cli/guards-integration.test.d.ts +2 -0
- package/dist/cli/guards-integration.test.d.ts.map +1 -0
- package/dist/cli/guards-integration.test.js +178 -0
- package/dist/cli/guards-integration.test.js.map +1 -0
- package/dist/cli/guards.d.ts +13 -0
- package/dist/cli/guards.d.ts.map +1 -0
- package/dist/cli/guards.js +70 -0
- package/dist/cli/guards.js.map +1 -0
- package/dist/cli/guards.test.d.ts +2 -0
- package/dist/cli/guards.test.d.ts.map +1 -0
- package/dist/cli/guards.test.js +136 -0
- package/dist/cli/guards.test.js.map +1 -0
- package/dist/cli/init-flag-families.d.ts +1 -1
- package/dist/cli/init-flag-families.d.ts.map +1 -1
- package/dist/cli/init-flag-families.js +4 -1
- package/dist/cli/init-flag-families.js.map +1 -1
- package/dist/cli/init-flag-families.test.js +10 -0
- package/dist/cli/init-flag-families.test.js.map +1 -1
- package/dist/cli/shutdown.d.ts +2 -3
- package/dist/cli/shutdown.d.ts.map +1 -1
- package/dist/cli/shutdown.js +14 -11
- package/dist/cli/shutdown.js.map +1 -1
- package/dist/cli/shutdown.test.js +2 -4
- package/dist/cli/shutdown.test.js.map +1 -1
- package/dist/config/schema.d.ts +12122 -288
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +74 -79
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +230 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/config/validators/backend.d.ts +4 -0
- package/dist/config/validators/backend.d.ts.map +1 -0
- package/dist/config/validators/backend.js +14 -0
- package/dist/config/validators/backend.js.map +1 -0
- package/dist/config/validators/browser-extension.d.ts +4 -0
- package/dist/config/validators/browser-extension.d.ts.map +1 -0
- package/dist/config/validators/browser-extension.js +24 -0
- package/dist/config/validators/browser-extension.js.map +1 -0
- package/dist/config/validators/cli.d.ts +4 -0
- package/dist/config/validators/cli.d.ts.map +1 -0
- package/dist/config/validators/cli.js +14 -0
- package/dist/config/validators/cli.js.map +1 -0
- package/dist/config/validators/data-pipeline.d.ts +4 -0
- package/dist/config/validators/data-pipeline.d.ts.map +1 -0
- package/dist/config/validators/data-pipeline.js +14 -0
- package/dist/config/validators/data-pipeline.js.map +1 -0
- package/dist/config/validators/game.d.ts +4 -0
- package/dist/config/validators/game.d.ts.map +1 -0
- package/dist/config/validators/game.js +14 -0
- package/dist/config/validators/game.js.map +1 -0
- package/dist/config/validators/index.d.ts +7 -0
- package/dist/config/validators/index.d.ts.map +1 -0
- package/dist/config/validators/index.js +27 -0
- package/dist/config/validators/index.js.map +1 -0
- package/dist/config/validators/library.d.ts +4 -0
- package/dist/config/validators/library.d.ts.map +1 -0
- package/dist/config/validators/library.js +25 -0
- package/dist/config/validators/library.js.map +1 -0
- package/dist/config/validators/ml.d.ts +4 -0
- package/dist/config/validators/ml.d.ts.map +1 -0
- package/dist/config/validators/ml.js +31 -0
- package/dist/config/validators/ml.js.map +1 -0
- package/dist/config/validators/mobile-app.d.ts +4 -0
- package/dist/config/validators/mobile-app.d.ts.map +1 -0
- package/dist/config/validators/mobile-app.js +14 -0
- package/dist/config/validators/mobile-app.js.map +1 -0
- package/dist/config/validators/registry.test.d.ts +2 -0
- package/dist/config/validators/registry.test.d.ts.map +1 -0
- package/dist/config/validators/registry.test.js +26 -0
- package/dist/config/validators/registry.test.js.map +1 -0
- package/dist/config/validators/research.d.ts +4 -0
- package/dist/config/validators/research.d.ts.map +1 -0
- package/dist/config/validators/research.js +24 -0
- package/dist/config/validators/research.js.map +1 -0
- package/dist/config/validators/research.test.d.ts +2 -0
- package/dist/config/validators/research.test.d.ts.map +1 -0
- package/dist/config/validators/research.test.js +44 -0
- package/dist/config/validators/research.test.js.map +1 -0
- package/dist/config/validators/types.d.ts +19 -0
- package/dist/config/validators/types.d.ts.map +1 -0
- package/dist/config/validators/types.js +2 -0
- package/dist/config/validators/types.js.map +1 -0
- package/dist/config/validators/validators.test.d.ts +2 -0
- package/dist/config/validators/validators.test.d.ts.map +1 -0
- package/dist/config/validators/validators.test.js +25 -0
- package/dist/config/validators/validators.test.js.map +1 -0
- package/dist/config/validators/web-app.d.ts +4 -0
- package/dist/config/validators/web-app.d.ts.map +1 -0
- package/dist/config/validators/web-app.js +31 -0
- package/dist/config/validators/web-app.js.map +1 -0
- package/dist/core/assembly/context-gatherer.d.ts.map +1 -1
- package/dist/core/assembly/context-gatherer.js +4 -2
- package/dist/core/assembly/context-gatherer.js.map +1 -1
- package/dist/core/assembly/cross-reads.d.ts +58 -0
- package/dist/core/assembly/cross-reads.d.ts.map +1 -0
- package/dist/core/assembly/cross-reads.js +185 -0
- package/dist/core/assembly/cross-reads.js.map +1 -0
- package/dist/core/assembly/cross-reads.test.d.ts +2 -0
- package/dist/core/assembly/cross-reads.test.d.ts.map +1 -0
- package/dist/core/assembly/cross-reads.test.js +383 -0
- package/dist/core/assembly/cross-reads.test.js.map +1 -0
- package/dist/core/assembly/overlay-loader-structural.test.d.ts +2 -0
- package/dist/core/assembly/overlay-loader-structural.test.d.ts.map +1 -0
- package/dist/core/assembly/overlay-loader-structural.test.js +114 -0
- package/dist/core/assembly/overlay-loader-structural.test.js.map +1 -0
- package/dist/core/assembly/overlay-loader.d.ts +17 -3
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +75 -0
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-resolver.d.ts +2 -2
- package/dist/core/assembly/overlay-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-resolver.js.map +1 -1
- package/dist/core/assembly/overlay-resolver.test.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.d.ts +5 -0
- package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.js +41 -1
- package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.test.js +262 -0
- package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
- package/dist/core/assembly/update-mode.d.ts +1 -0
- package/dist/core/assembly/update-mode.d.ts.map +1 -1
- package/dist/core/assembly/update-mode.js +17 -9
- package/dist/core/assembly/update-mode.js.map +1 -1
- package/dist/core/dependency/eligibility.d.ts +10 -1
- package/dist/core/dependency/eligibility.d.ts.map +1 -1
- package/dist/core/dependency/eligibility.js +19 -1
- package/dist/core/dependency/eligibility.js.map +1 -1
- package/dist/core/dependency/eligibility.test.js +82 -0
- package/dist/core/dependency/eligibility.test.js.map +1 -1
- package/dist/core/dependency/graph.d.ts +4 -1
- package/dist/core/dependency/graph.d.ts.map +1 -1
- package/dist/core/dependency/graph.js +7 -1
- package/dist/core/dependency/graph.js.map +1 -1
- package/dist/core/dependency/graph.test.js +29 -0
- package/dist/core/dependency/graph.test.js.map +1 -1
- package/dist/core/pipeline/global-steps.d.ts +7 -0
- package/dist/core/pipeline/global-steps.d.ts.map +1 -0
- package/dist/core/pipeline/global-steps.js +18 -0
- package/dist/core/pipeline/global-steps.js.map +1 -0
- package/dist/core/pipeline/resolver.d.ts +1 -0
- package/dist/core/pipeline/resolver.d.ts.map +1 -1
- package/dist/core/pipeline/resolver.js +51 -6
- package/dist/core/pipeline/resolver.js.map +1 -1
- package/dist/core/pipeline/types.d.ts +5 -1
- package/dist/core/pipeline/types.d.ts.map +1 -1
- package/dist/e2e/cross-service-references.test.d.ts +22 -0
- package/dist/e2e/cross-service-references.test.d.ts.map +1 -0
- package/dist/e2e/cross-service-references.test.js +188 -0
- package/dist/e2e/cross-service-references.test.js.map +1 -0
- package/dist/e2e/multi-service-pipeline.test.d.ts +10 -0
- package/dist/e2e/multi-service-pipeline.test.d.ts.map +1 -0
- package/dist/e2e/multi-service-pipeline.test.js +185 -0
- package/dist/e2e/multi-service-pipeline.test.js.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +68 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/e2e/service-execution.test.d.ts +15 -0
- package/dist/e2e/service-execution.test.d.ts.map +1 -0
- package/dist/e2e/service-execution.test.js +219 -0
- package/dist/e2e/service-execution.test.js.map +1 -0
- package/dist/e2e/service-manifest.test.d.ts +19 -0
- package/dist/e2e/service-manifest.test.d.ts.map +1 -0
- package/dist/e2e/service-manifest.test.js +166 -0
- package/dist/e2e/service-manifest.test.js.map +1 -0
- package/dist/project/__frozen-schemas__/schema-v3.9.2.d.ts +224 -224
- package/dist/project/frontmatter.d.ts.map +1 -1
- package/dist/project/frontmatter.js +11 -0
- package/dist/project/frontmatter.js.map +1 -1
- package/dist/project/frontmatter.test.js +71 -0
- package/dist/project/frontmatter.test.js.map +1 -1
- package/dist/state/completion.d.ts +1 -1
- package/dist/state/completion.d.ts.map +1 -1
- package/dist/state/completion.js +10 -8
- package/dist/state/completion.js.map +1 -1
- package/dist/state/decision-logger.d.ts +3 -2
- package/dist/state/decision-logger.d.ts.map +1 -1
- package/dist/state/decision-logger.js +12 -11
- package/dist/state/decision-logger.js.map +1 -1
- package/dist/state/ensure-v3-migration.d.ts +9 -0
- package/dist/state/ensure-v3-migration.d.ts.map +1 -0
- package/dist/state/ensure-v3-migration.js +35 -0
- package/dist/state/ensure-v3-migration.js.map +1 -0
- package/dist/state/lock-manager.d.ts +5 -4
- package/dist/state/lock-manager.d.ts.map +1 -1
- package/dist/state/lock-manager.js +11 -11
- package/dist/state/lock-manager.js.map +1 -1
- package/dist/state/rework-manager.d.ts +1 -2
- package/dist/state/rework-manager.d.ts.map +1 -1
- package/dist/state/rework-manager.js +4 -5
- package/dist/state/rework-manager.js.map +1 -1
- package/dist/state/state-manager.d.ts +25 -1
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +86 -12
- package/dist/state/state-manager.js.map +1 -1
- package/dist/state/state-manager.test.js +278 -0
- package/dist/state/state-manager.test.js.map +1 -1
- package/dist/state/state-migration-v3.d.ts +22 -0
- package/dist/state/state-migration-v3.d.ts.map +1 -0
- package/dist/state/state-migration-v3.js +82 -0
- package/dist/state/state-migration-v3.js.map +1 -0
- package/dist/state/state-migration-v3.test.d.ts +2 -0
- package/dist/state/state-migration-v3.test.d.ts.map +1 -0
- package/dist/state/state-migration-v3.test.js +196 -0
- package/dist/state/state-migration-v3.test.js.map +1 -0
- package/dist/state/state-migration.d.ts.map +1 -1
- package/dist/state/state-migration.js +11 -6
- package/dist/state/state-migration.js.map +1 -1
- package/dist/state/state-migration.test.js +47 -2
- package/dist/state/state-migration.test.js.map +1 -1
- package/dist/state/state-path-resolver.d.ts +23 -0
- package/dist/state/state-path-resolver.d.ts.map +1 -0
- package/dist/state/state-path-resolver.js +36 -0
- package/dist/state/state-path-resolver.js.map +1 -0
- package/dist/state/state-path-resolver.test.d.ts +2 -0
- package/dist/state/state-path-resolver.test.d.ts.map +1 -0
- package/dist/state/state-path-resolver.test.js +78 -0
- package/dist/state/state-path-resolver.test.js.map +1 -0
- package/dist/state/state-version-dispatch.d.ts +17 -0
- package/dist/state/state-version-dispatch.d.ts.map +1 -0
- package/dist/state/state-version-dispatch.js +27 -0
- package/dist/state/state-version-dispatch.js.map +1 -0
- package/dist/state/state-version-dispatch.test.d.ts +2 -0
- package/dist/state/state-version-dispatch.test.d.ts.map +1 -0
- package/dist/state/state-version-dispatch.test.js +40 -0
- package/dist/state/state-version-dispatch.test.js.map +1 -0
- package/dist/types/config.d.ts +25 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.test.js +13 -1
- package/dist/types/config.test.js.map +1 -1
- package/dist/types/dependency.d.ts +5 -0
- package/dist/types/dependency.d.ts.map +1 -1
- package/dist/types/frontmatter.d.ts +5 -0
- package/dist/types/frontmatter.d.ts.map +1 -1
- package/dist/types/lock.d.ts +1 -1
- package/dist/types/lock.d.ts.map +1 -1
- package/dist/types/state.d.ts +1 -1
- package/dist/types/state.d.ts.map +1 -1
- package/dist/utils/artifact-path.d.ts +19 -0
- package/dist/utils/artifact-path.d.ts.map +1 -0
- package/dist/utils/artifact-path.js +95 -0
- package/dist/utils/artifact-path.js.map +1 -0
- package/dist/utils/artifact-path.test.d.ts +2 -0
- package/dist/utils/artifact-path.test.d.ts.map +1 -0
- package/dist/utils/artifact-path.test.js +138 -0
- package/dist/utils/artifact-path.test.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +5 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/user-errors.d.ts +46 -0
- package/dist/utils/user-errors.d.ts.map +1 -0
- package/dist/utils/user-errors.js +76 -0
- package/dist/utils/user-errors.js.map +1 -0
- package/dist/utils/user-errors.test.d.ts +2 -0
- package/dist/utils/user-errors.test.d.ts.map +1 -0
- package/dist/utils/user-errors.test.js +74 -0
- package/dist/utils/user-errors.test.js.map +1 -0
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +16 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.test.js +48 -0
- package/dist/validation/index.test.js.map +1 -1
- package/dist/validation/state-validator.d.ts +5 -2
- package/dist/validation/state-validator.d.ts.map +1 -1
- package/dist/validation/state-validator.js +18 -20
- package/dist/validation/state-validator.js.map +1 -1
- package/dist/validation/state-validator.test.js +31 -2
- package/dist/validation/state-validator.test.js.map +1 -1
- package/dist/wizard/copy/backend.d.ts.map +1 -1
- package/dist/wizard/copy/backend.js +12 -0
- package/dist/wizard/copy/backend.js.map +1 -1
- package/dist/wizard/flags.d.ts +1 -0
- package/dist/wizard/flags.d.ts.map +1 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +5 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +45 -2
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +23 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +85 -47
- package/dist/wizard/wizard.js.map +1 -1
- package/dist/wizard/wizard.test.js +186 -1
- package/dist/wizard/wizard.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multi-service-api-contracts
|
|
3
|
+
description: Internal API versioning, backward compatibility, retries, and idempotency patterns
|
|
4
|
+
topics: [internal-api-versioning, backward-compatibility, retries, idempotency, contract-evolution]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Internal APIs between services must evolve without coordinated deployments — service A cannot require service B to deploy simultaneously. This demands strict backward compatibility, well-defined deprecation timelines, retry policies that assume transient failures, and idempotency patterns that make retries safe.
|
|
10
|
+
|
|
11
|
+
**Versioning strategies:**
|
|
12
|
+
- **URL path versioning** (`/api/v1/`, `/api/v2/`): most discoverable, recommended for new services.
|
|
13
|
+
- **Header-based versioning** (`X-API-Version`): stable URLs, better for webhooks and internal service APIs.
|
|
14
|
+
- **Content negotiation** (`Accept: application/vnd.acme.v2+json`): strictest REST compliance.
|
|
15
|
+
|
|
16
|
+
**Backward compatibility rules:** Adding fields, endpoints, or optional parameters is safe. Removing fields, renaming fields, changing types, or making optional parameters required requires a new version.
|
|
17
|
+
|
|
18
|
+
**Retry policy:** Retry only on transient errors (5xx, 408, 429). Never retry non-idempotent POSTs without an idempotency key. Always add exponential backoff with jitter to prevent thundering herd. Combine with a circuit breaker so a failing service isn't hammered during recovery.
|
|
19
|
+
|
|
20
|
+
**Idempotency keys:** Clients generate keys (UUID v4) scoped to the operation. Servers deduplicate by storing the key and returning the cached response on replay. Keys expire after 24-72 hours.
|
|
21
|
+
|
|
22
|
+
**Timeout budgets:** Each call chain has a total budget (e.g., 2000ms). Propagate the absolute deadline via `x-request-deadline` header so all services share one clock rather than stacking per-hop timeouts.
|
|
23
|
+
|
|
24
|
+
**Deprecation:** Minimum 90 days for internal APIs. Announce via `Deprecation` and `Sunset` HTTP response headers. Log all usage of deprecated endpoints to identify consumers who need to migrate.
|
|
25
|
+
|
|
26
|
+
## Deep Guidance
|
|
27
|
+
|
|
28
|
+
## Internal API Versioning Strategies
|
|
29
|
+
|
|
30
|
+
### URL Path Versioning
|
|
31
|
+
|
|
32
|
+
The simplest and most discoverable approach: the version is part of the URL path.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
GET /api/v1/orders/:id
|
|
36
|
+
GET /api/v2/orders/:id
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Service B routes traffic by URL prefix. Both versions run simultaneously during migration. Service A upgrades to v2 at its own pace. Once all consumers are on v2, v1 is decommissioned.
|
|
40
|
+
|
|
41
|
+
**Trade-offs:**
|
|
42
|
+
- (+) Version is visible in logs, dashboards, and access logs — trivially easy to spot which version is being called.
|
|
43
|
+
- (+) Easy to test: curl a specific version without special headers.
|
|
44
|
+
- (+) Cacheable at every layer (CDN, reverse proxy, client cache).
|
|
45
|
+
- (-) URL proliferation — resource paths are repeated for each version.
|
|
46
|
+
- (-) Clients must update hard-coded URLs when migrating versions.
|
|
47
|
+
- (-) Encourages big-bang versioning rather than additive evolution.
|
|
48
|
+
|
|
49
|
+
**When to use:** Public-facing APIs, APIs consumed by many independent teams, or when clear migration paths are more important than URL aesthetics.
|
|
50
|
+
|
|
51
|
+
### Header-Based Versioning
|
|
52
|
+
|
|
53
|
+
The version is communicated through a request header, keeping URLs stable.
|
|
54
|
+
|
|
55
|
+
```http
|
|
56
|
+
GET /orders/abc123
|
|
57
|
+
Accept: application/vnd.myservice.v2+json
|
|
58
|
+
# or
|
|
59
|
+
GET /orders/abc123
|
|
60
|
+
X-API-Version: 2
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The service inspects the header and dispatches to the appropriate handler version. The URL is the same across versions.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// Express middleware for header-based versioning
|
|
67
|
+
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
68
|
+
const version = req.headers['x-api-version'] as string
|
|
69
|
+
|| req.headers['accept']?.match(/vnd\.myservice\.v(\d+)/)?.[1]
|
|
70
|
+
|| '1'; // default to v1 if omitted
|
|
71
|
+
|
|
72
|
+
req.apiVersion = parseInt(version, 10);
|
|
73
|
+
|
|
74
|
+
if (req.apiVersion > MAX_SUPPORTED_VERSION) {
|
|
75
|
+
return res.status(400).json({
|
|
76
|
+
error: { code: 'UNSUPPORTED_VERSION', message: `Maximum supported version is ${MAX_SUPPORTED_VERSION}` }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
next();
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Trade-offs:**
|
|
85
|
+
- (+) Clean, stable URLs. Bookmarks and links don't break when the version changes.
|
|
86
|
+
- (+) Multiple versions can be differentiated at the handler level without separate route registrations.
|
|
87
|
+
- (-) Not visible in browser URL bar. Harder to test without tooling.
|
|
88
|
+
- (-) Cannot be cached by standard reverse proxies without custom `Vary` headers.
|
|
89
|
+
- (-) Easy to forget the header, leading to silent version defaulting.
|
|
90
|
+
|
|
91
|
+
**When to use:** Internal service-to-service APIs where both sides are under your control and you can enforce consistent header setting in your service clients.
|
|
92
|
+
|
|
93
|
+
### Content Negotiation (Media Type Versioning)
|
|
94
|
+
|
|
95
|
+
A stricter form of header versioning using standard HTTP `Accept` and `Content-Type` headers with vendor-specific media types.
|
|
96
|
+
|
|
97
|
+
```http
|
|
98
|
+
GET /orders/abc123
|
|
99
|
+
Accept: application/vnd.acme.orders-v2+json
|
|
100
|
+
|
|
101
|
+
# Response
|
|
102
|
+
Content-Type: application/vnd.acme.orders-v2+json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Trade-offs:**
|
|
106
|
+
- (+) Uses HTTP standards correctly. No custom header conventions to document.
|
|
107
|
+
- (+) Fine-grained: can version individual resource types independently.
|
|
108
|
+
- (-) Most unfamiliar to developers accustomed to URL versioning.
|
|
109
|
+
- (-) Verbose headers. More complex client setup.
|
|
110
|
+
- (-) Media type parsing is fragile if not done carefully.
|
|
111
|
+
|
|
112
|
+
**When to use:** APIs adhering strictly to REST constraints, or when you need to version individual resource representations independently of the endpoint.
|
|
113
|
+
|
|
114
|
+
### Choosing a Versioning Strategy
|
|
115
|
+
|
|
116
|
+
For internal multi-service APIs: **prefer URL path versioning for new services** and header versioning when URLs must remain stable (e.g., webhooks, callback URLs, shared bookmarked resources). Both work. Consistency within a system matters more than the strategy chosen.
|
|
117
|
+
|
|
118
|
+
## Backward Compatibility Rules
|
|
119
|
+
|
|
120
|
+
### Additive-Only Changes (Non-Breaking)
|
|
121
|
+
|
|
122
|
+
The following changes are backward compatible and do not require a new version:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
✓ Adding a new field to a response body
|
|
126
|
+
✓ Adding a new optional query parameter
|
|
127
|
+
✓ Adding a new endpoint
|
|
128
|
+
✓ Adding a new enum value (consumers must handle unknown enum values gracefully)
|
|
129
|
+
✓ Adding a new HTTP header to responses
|
|
130
|
+
✓ Relaxing a validation constraint (accepting more values than before)
|
|
131
|
+
✓ Making a previously required field optional
|
|
132
|
+
✓ Adding a new error code to the set
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Example of a backward-compatible response evolution:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
// v1 response
|
|
139
|
+
{
|
|
140
|
+
"orderId": "ord_abc123",
|
|
141
|
+
"status": "confirmed",
|
|
142
|
+
"total": 9900
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// v1.1 (still v1, additive change) — consumers that don't know about new fields ignore them
|
|
146
|
+
{
|
|
147
|
+
"orderId": "ord_abc123",
|
|
148
|
+
"status": "confirmed",
|
|
149
|
+
"total": 9900,
|
|
150
|
+
"currency": "USD",
|
|
151
|
+
"estimatedDelivery": "2026-04-18"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Breaking Changes (Version Bump Required)
|
|
156
|
+
|
|
157
|
+
The following changes are breaking and require a new version or a coordinated migration:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
✗ Removing a field from a response body
|
|
161
|
+
✗ Renaming a field
|
|
162
|
+
✗ Changing a field's type (string → integer, object → array)
|
|
163
|
+
✗ Making an optional parameter required
|
|
164
|
+
✗ Changing the URL structure of an existing endpoint
|
|
165
|
+
✗ Changing the meaning of an existing status code
|
|
166
|
+
✗ Removing a valid enum value
|
|
167
|
+
✗ Changing error response structure
|
|
168
|
+
✗ Narrowing a validation constraint (accepting fewer values than before)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Consumer Robustness Rules
|
|
172
|
+
|
|
173
|
+
Consumers (clients calling an API) must be written to tolerate additive changes without code modifications:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// BAD: Destructuring that fails if a new field is present (strict TypeScript)
|
|
177
|
+
const { orderId, status, total } = response; // fails if response has extra fields
|
|
178
|
+
|
|
179
|
+
// GOOD: Pick only what you need — unknown fields are ignored
|
|
180
|
+
interface OrderResponse {
|
|
181
|
+
orderId: string;
|
|
182
|
+
status: OrderStatus;
|
|
183
|
+
total: number;
|
|
184
|
+
// deliberately does not enumerate all fields
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// BAD: Switch on enum that throws on unknown values
|
|
188
|
+
switch (order.status) {
|
|
189
|
+
case 'pending': handlePending(); break;
|
|
190
|
+
case 'confirmed': handleConfirmed(); break;
|
|
191
|
+
default: throw new Error(`Unknown status: ${order.status}`); // BREAKS on new enum values
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// GOOD: Default case handles unknown enum values gracefully
|
|
195
|
+
switch (order.status) {
|
|
196
|
+
case 'pending': handlePending(); break;
|
|
197
|
+
case 'confirmed': handleConfirmed(); break;
|
|
198
|
+
default:
|
|
199
|
+
logger.warn({ status: order.status }, 'Received unknown order status — treating as pending');
|
|
200
|
+
handlePending();
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Deprecation Timelines
|
|
205
|
+
|
|
206
|
+
When a field, endpoint, or behavior is deprecated:
|
|
207
|
+
|
|
208
|
+
1. **Announce deprecation** — add `Deprecation` and `Sunset` HTTP headers to responses.
|
|
209
|
+
2. **Log usage** — log every request using the deprecated feature so consumers can be identified.
|
|
210
|
+
3. **Notify consumers** — use logs and headers to identify and reach affected teams.
|
|
211
|
+
4. **Honor the sunset date** — remove the deprecated feature no earlier than the sunset date.
|
|
212
|
+
|
|
213
|
+
```http
|
|
214
|
+
HTTP/1.1 200 OK
|
|
215
|
+
Deprecation: true
|
|
216
|
+
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
|
|
217
|
+
Link: <https://docs.internal/api/v2/migration>; rel="successor-version"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Minimum deprecation windows:**
|
|
221
|
+
- Internal services (same organization): 90 days
|
|
222
|
+
- Partner APIs: 180 days
|
|
223
|
+
- Public APIs: 365 days
|
|
224
|
+
|
|
225
|
+
## Retry Policies with Exponential Backoff and Jitter
|
|
226
|
+
|
|
227
|
+
### When to Retry
|
|
228
|
+
|
|
229
|
+
Retry only on transient errors. Distinguish transient from permanent failures:
|
|
230
|
+
|
|
231
|
+
| HTTP Status | Retry? | Rationale |
|
|
232
|
+
|-------------|--------|-----------|
|
|
233
|
+
| 408 Request Timeout | Yes | Server-side timeout — transient |
|
|
234
|
+
| 429 Too Many Requests | Yes, with backoff | Rate limit — respect `Retry-After` |
|
|
235
|
+
| 500 Internal Server Error | Conditionally | Transient server error — retry if idempotent |
|
|
236
|
+
| 502 Bad Gateway | Yes | Upstream unavailable — transient |
|
|
237
|
+
| 503 Service Unavailable | Yes | Server overload — transient |
|
|
238
|
+
| 504 Gateway Timeout | Yes | Upstream timeout — transient |
|
|
239
|
+
| 400 Bad Request | No | Client error — retrying won't help |
|
|
240
|
+
| 401 Unauthorized | No | Auth failure — re-authenticate, don't retry |
|
|
241
|
+
| 403 Forbidden | No | Permission denied — permanent |
|
|
242
|
+
| 404 Not Found | No | Resource absent — permanent |
|
|
243
|
+
| 409 Conflict | No | State conflict — permanent (usually) |
|
|
244
|
+
| 422 Unprocessable Entity | No | Validation failure — permanent |
|
|
245
|
+
|
|
246
|
+
**Critical rule:** Only retry idempotent operations, or operations that use idempotency keys. Retrying a non-idempotent POST without an idempotency key risks duplicate side effects (double charges, duplicate orders).
|
|
247
|
+
|
|
248
|
+
### Exponential Backoff with Jitter
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
interface RetryConfig {
|
|
252
|
+
maxAttempts: number; // Total attempts (including first)
|
|
253
|
+
baseDelayMs: number; // Initial delay
|
|
254
|
+
maxDelayMs: number; // Cap on delay growth
|
|
255
|
+
jitterFactor: number; // 0.0 to 1.0 — adds randomness to prevent thundering herd
|
|
256
|
+
retryableStatusCodes: Set<number>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
260
|
+
maxAttempts: 3,
|
|
261
|
+
baseDelayMs: 100,
|
|
262
|
+
maxDelayMs: 30_000,
|
|
263
|
+
jitterFactor: 0.5,
|
|
264
|
+
retryableStatusCodes: new Set([408, 429, 500, 502, 503, 504]),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
function calculateDelay(attempt: number, config: RetryConfig): number {
|
|
268
|
+
// Exponential: 100ms, 200ms, 400ms, 800ms...
|
|
269
|
+
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
270
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
271
|
+
|
|
272
|
+
// Full jitter: random value between 0 and cappedDelay * jitterFactor
|
|
273
|
+
const jitter = Math.random() * cappedDelay * config.jitterFactor;
|
|
274
|
+
|
|
275
|
+
return cappedDelay - jitter; // Decorrelated jitter reduces thundering herd
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function withRetry<T>(
|
|
279
|
+
fn: () => Promise<T>,
|
|
280
|
+
config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
281
|
+
): Promise<T> {
|
|
282
|
+
let lastError: Error | undefined;
|
|
283
|
+
|
|
284
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
285
|
+
try {
|
|
286
|
+
return await fn();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
lastError = error as Error;
|
|
289
|
+
|
|
290
|
+
// Extract HTTP status if available
|
|
291
|
+
const status = (error as any)?.response?.status;
|
|
292
|
+
|
|
293
|
+
// Don't retry non-transient errors
|
|
294
|
+
if (status && !config.retryableStatusCodes.has(status)) {
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Don't sleep after the last attempt
|
|
299
|
+
if (attempt === config.maxAttempts) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Respect server-provided Retry-After header
|
|
304
|
+
const retryAfter = (error as any)?.response?.headers?.['retry-after'];
|
|
305
|
+
const delay = retryAfter
|
|
306
|
+
? parseInt(retryAfter, 10) * 1000
|
|
307
|
+
: calculateDelay(attempt, config);
|
|
308
|
+
|
|
309
|
+
logger.warn({ attempt, delay, status }, 'Retrying after transient failure');
|
|
310
|
+
await sleep(delay);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw lastError;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Circuit Breaker Pattern
|
|
319
|
+
|
|
320
|
+
Retries alone are insufficient. Without a circuit breaker, retries against a failing downstream service amplify load during an outage, making recovery harder.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
type CircuitState = 'closed' | 'open' | 'half-open';
|
|
324
|
+
|
|
325
|
+
interface CircuitBreakerConfig {
|
|
326
|
+
failureThreshold: number; // Failures before opening
|
|
327
|
+
successThreshold: number; // Successes in half-open before closing
|
|
328
|
+
timeoutMs: number; // Time open before trying half-open
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class CircuitBreaker {
|
|
332
|
+
private state: CircuitState = 'closed';
|
|
333
|
+
private failures = 0;
|
|
334
|
+
private successes = 0;
|
|
335
|
+
private lastFailureTime?: number;
|
|
336
|
+
|
|
337
|
+
constructor(private config: CircuitBreakerConfig) {}
|
|
338
|
+
|
|
339
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
340
|
+
if (this.state === 'open') {
|
|
341
|
+
const elapsed = Date.now() - (this.lastFailureTime ?? 0);
|
|
342
|
+
if (elapsed > this.config.timeoutMs) {
|
|
343
|
+
this.state = 'half-open';
|
|
344
|
+
} else {
|
|
345
|
+
throw new Error('Circuit open — downstream service unavailable');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const result = await fn();
|
|
351
|
+
this.onSuccess();
|
|
352
|
+
return result;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.onFailure();
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private onSuccess() {
|
|
360
|
+
if (this.state === 'half-open') {
|
|
361
|
+
this.successes++;
|
|
362
|
+
if (this.successes >= this.config.successThreshold) {
|
|
363
|
+
this.state = 'closed';
|
|
364
|
+
this.failures = 0;
|
|
365
|
+
this.successes = 0;
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
this.failures = 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private onFailure() {
|
|
373
|
+
this.failures++;
|
|
374
|
+
this.lastFailureTime = Date.now();
|
|
375
|
+
if (this.failures >= this.config.failureThreshold) {
|
|
376
|
+
this.state = 'open';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Trade-offs (retry + circuit breaker):**
|
|
383
|
+
- (+) Transient failures are transparent to callers — retries recover without caller awareness.
|
|
384
|
+
- (+) Circuit breaker prevents cascading failures and gives failing services time to recover.
|
|
385
|
+
- (-) Retries increase tail latency — a 3-retry call can take 3x the timeout budget.
|
|
386
|
+
- (-) Jitter adds complexity. Without jitter, retries from many clients synchronize and create thundering herd.
|
|
387
|
+
|
|
388
|
+
## Idempotency Key Design Patterns
|
|
389
|
+
|
|
390
|
+
### Why Idempotency Keys Are Required
|
|
391
|
+
|
|
392
|
+
In distributed systems, at-most-once delivery is not achievable without distributed consensus (expensive). At-least-once delivery is the practical default. This means:
|
|
393
|
+
|
|
394
|
+
- Network requests may be retried
|
|
395
|
+
- Message queue consumers may receive duplicates
|
|
396
|
+
- Webhooks may be delivered multiple times
|
|
397
|
+
|
|
398
|
+
Every mutation operation must be idempotent to handle these cases safely.
|
|
399
|
+
|
|
400
|
+
### Idempotency Key Protocol
|
|
401
|
+
|
|
402
|
+
```http
|
|
403
|
+
POST /payments
|
|
404
|
+
Idempotency-Key: idk_7f3e2a1c-9d4b-4e6f-a8c2-1234567890ab
|
|
405
|
+
Content-Type: application/json
|
|
406
|
+
|
|
407
|
+
{
|
|
408
|
+
"amount": 9900,
|
|
409
|
+
"currency": "USD",
|
|
410
|
+
"customerId": "cust_abc123"
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Server-side implementation:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
interface IdempotencyRecord {
|
|
418
|
+
key: string;
|
|
419
|
+
requestHash: string; // Hash of request body to detect mismatched replays
|
|
420
|
+
status: 'processing' | 'complete' | 'error';
|
|
421
|
+
response: unknown;
|
|
422
|
+
statusCode: number;
|
|
423
|
+
expiresAt: Date;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function handleWithIdempotency(
|
|
427
|
+
req: Request,
|
|
428
|
+
res: Response,
|
|
429
|
+
handler: () => Promise<{ statusCode: number; body: unknown }>,
|
|
430
|
+
) {
|
|
431
|
+
const idempotencyKey = req.headers['idempotency-key'] as string | undefined;
|
|
432
|
+
|
|
433
|
+
if (!idempotencyKey) {
|
|
434
|
+
// Idempotency key is optional but recommended — proceed without deduplication
|
|
435
|
+
const { statusCode, body } = await handler();
|
|
436
|
+
return res.status(statusCode).json(body);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate key format
|
|
440
|
+
if (!/^[a-zA-Z0-9_-]{8,128}$/.test(idempotencyKey)) {
|
|
441
|
+
return res.status(400).json({ error: { code: 'INVALID_IDEMPOTENCY_KEY' } });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Compute hash of request body to detect mismatched replays
|
|
445
|
+
const requestHash = crypto
|
|
446
|
+
.createHash('sha256')
|
|
447
|
+
.update(JSON.stringify(req.body))
|
|
448
|
+
.digest('hex');
|
|
449
|
+
|
|
450
|
+
// Atomic check-and-set to handle concurrent requests with the same key
|
|
451
|
+
const existing = await idempotencyStore.getOrCreate(idempotencyKey, {
|
|
452
|
+
requestHash,
|
|
453
|
+
status: 'processing',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (existing) {
|
|
457
|
+
// Replay detected
|
|
458
|
+
if (existing.requestHash !== requestHash) {
|
|
459
|
+
return res.status(422).json({
|
|
460
|
+
error: { code: 'IDEMPOTENCY_KEY_REUSE', message: 'Key used with different request body' }
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (existing.status === 'processing') {
|
|
465
|
+
return res.status(409).json({
|
|
466
|
+
error: { code: 'CONCURRENT_REQUEST', message: 'A request with this key is in progress' }
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Return stored response
|
|
471
|
+
return res.status(existing.statusCode).json(existing.response);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// First request — process and store result
|
|
475
|
+
try {
|
|
476
|
+
const { statusCode, body } = await handler();
|
|
477
|
+
await idempotencyStore.complete(idempotencyKey, { statusCode, response: body });
|
|
478
|
+
return res.status(statusCode).json(body);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
await idempotencyStore.error(idempotencyKey, { statusCode: 500, response: { error: 'Internal error' } });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Key Generation Guidelines
|
|
487
|
+
|
|
488
|
+
- Clients generate idempotency keys, not servers. This ensures keys survive network failures.
|
|
489
|
+
- Use UUIDs (v4) or crypto-random strings of at least 128 bits of entropy.
|
|
490
|
+
- Scope keys to the operation: `pay_<uuid>` for payments, `ord_<uuid>` for orders. Avoids cross-operation key collisions.
|
|
491
|
+
- Keys expire after 24–72 hours. Document the expiry window clearly.
|
|
492
|
+
- Store keys in a fast, durable store (Redis with persistence, or a database table with an index on the key).
|
|
493
|
+
|
|
494
|
+
**Trade-offs (idempotency keys):**
|
|
495
|
+
- (+) Retries are safe — the consumer doesn't need to worry about duplicate side effects.
|
|
496
|
+
- (+) Keys serve as a natural audit trail for retry behavior.
|
|
497
|
+
- (-) Requires durable storage and an atomic check-and-set operation.
|
|
498
|
+
- (-) Concurrent requests with the same key require careful locking (use database-level uniqueness constraints or Redis SET NX).
|
|
499
|
+
- (-) Keys must expire to avoid unbounded storage growth.
|
|
500
|
+
|
|
501
|
+
## Contract Evolution and Deprecation Strategies
|
|
502
|
+
|
|
503
|
+
### Evolutionary API Design
|
|
504
|
+
|
|
505
|
+
Design APIs to evolve without version bumps by applying these principles from the start:
|
|
506
|
+
|
|
507
|
+
**Use open content models:** Response objects should accept and ignore unknown fields. Never validate that a response contains only known fields.
|
|
508
|
+
|
|
509
|
+
**Use extension points:** Include a `metadata` or `extensions` field for future optional data:
|
|
510
|
+
|
|
511
|
+
```json
|
|
512
|
+
{
|
|
513
|
+
"orderId": "ord_abc123",
|
|
514
|
+
"status": "confirmed",
|
|
515
|
+
"metadata": {}
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Use stable identifiers:** Resource IDs must never change format. If you start with integer IDs, you cannot switch to UUIDs without a breaking change. Prefer opaque string identifiers from day one.
|
|
520
|
+
|
|
521
|
+
**Explicit nullability:** Distinguish "field is absent" from "field is explicitly null." A field that is absent means the server doesn't know about it; a field that is null means the server knows it has no value.
|
|
522
|
+
|
|
523
|
+
### Deprecation Workflow
|
|
524
|
+
|
|
525
|
+
1. **Mark deprecated in the schema:** Add `deprecated: true` in OpenAPI, or a `@deprecated` directive in GraphQL.
|
|
526
|
+
2. **Return deprecation headers:** Include `Deprecation` and `Sunset` headers.
|
|
527
|
+
3. **Emit metrics:** Record usage of deprecated endpoints/fields by consumer identity.
|
|
528
|
+
4. **Notify consumers:** Use the metrics to identify and notify affected service teams.
|
|
529
|
+
5. **Sunset:** On the sunset date, remove the deprecated feature. Return `410 Gone` for deprecated endpoints.
|
|
530
|
+
|
|
531
|
+
```yaml
|
|
532
|
+
# OpenAPI deprecation marking
|
|
533
|
+
paths:
|
|
534
|
+
/api/v1/orders:
|
|
535
|
+
get:
|
|
536
|
+
deprecated: true
|
|
537
|
+
description: "Deprecated. Use /api/v2/orders. Sunset: 2026-12-31."
|
|
538
|
+
x-sunset: "2026-12-31"
|
|
539
|
+
x-migration-guide: "https://docs.internal/api/v2/migration"
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Parallel Running Strategy
|
|
543
|
+
|
|
544
|
+
Run old and new versions simultaneously during migration:
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
Phase 1: Deploy v2 endpoint. Keep v1 live. No consumer changes.
|
|
548
|
+
Phase 2: Notify consumers. Give them 90 days to migrate.
|
|
549
|
+
Phase 3: Consumers migrate to v2 at their own pace.
|
|
550
|
+
Phase 4: Monitor v1 usage until it drops to zero.
|
|
551
|
+
Phase 5: Sunset v1. Return 410 for 30 days, then remove completely.
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Trade-offs:**
|
|
555
|
+
- (+) Zero forced lockstep deployments. Each team migrates independently.
|
|
556
|
+
- (+) Rollback is straightforward: revert client to v1 calls.
|
|
557
|
+
- (-) Running two versions doubles the code to maintain during the transition window.
|
|
558
|
+
- (-) Bugs must be fixed in both versions during the parallel period.
|
|
559
|
+
|
|
560
|
+
## Timeout Budget Allocation
|
|
561
|
+
|
|
562
|
+
### Timeout Budgets in Synchronous Call Chains
|
|
563
|
+
|
|
564
|
+
Every synchronous call chain has a total time budget (typically driven by the user-facing SLA). Allocate that budget across service hops:
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
User SLA: 2000ms total
|
|
568
|
+
|
|
569
|
+
API Gateway: 50ms (routing, auth)
|
|
570
|
+
BFF / Aggregator: 100ms (orchestration overhead)
|
|
571
|
+
→ Order Service: 500ms (own processing)
|
|
572
|
+
→ Inventory: 200ms (sub-call from Order)
|
|
573
|
+
→ User Service: 300ms (parallel with Order)
|
|
574
|
+
→ Catalog Service: 300ms (parallel with Order)
|
|
575
|
+
Buffer / P99 margin: 550ms
|
|
576
|
+
|
|
577
|
+
Total: 2000ms
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Rules:**
|
|
581
|
+
- Each service subtracts its own processing time from the budget and passes the remainder to downstream calls.
|
|
582
|
+
- Use deadline propagation: pass the absolute deadline as a header so all services in the chain share a single clock.
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
// Propagate deadline through service calls
|
|
586
|
+
function propagateDeadline(req: Request, outgoingHeaders: Record<string, string>) {
|
|
587
|
+
const incomingDeadline = req.headers['x-request-deadline'] as string | undefined;
|
|
588
|
+
if (incomingDeadline) {
|
|
589
|
+
const remainingMs = new Date(incomingDeadline).getTime() - Date.now();
|
|
590
|
+
if (remainingMs <= 0) {
|
|
591
|
+
throw new Error('Deadline exceeded before downstream call');
|
|
592
|
+
}
|
|
593
|
+
outgoingHeaders['x-request-deadline'] = incomingDeadline;
|
|
594
|
+
outgoingHeaders['x-request-timeout'] = String(Math.min(remainingMs - 10, DEFAULT_TIMEOUT));
|
|
595
|
+
} else {
|
|
596
|
+
const deadline = new Date(Date.now() + DEFAULT_TIMEOUT).toISOString();
|
|
597
|
+
outgoingHeaders['x-request-deadline'] = deadline;
|
|
598
|
+
outgoingHeaders['x-request-timeout'] = String(DEFAULT_TIMEOUT);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Trade-offs:**
|
|
604
|
+
- (+) Budget allocation prevents a slow upstream from consuming 100% of the budget, starving downstream calls.
|
|
605
|
+
- (+) Deadline propagation ensures the full chain fails fast when the overall budget is exhausted.
|
|
606
|
+
- (-) Requires consistent adoption across all services in the chain.
|
|
607
|
+
- (-) Hard-coded timeout budgets become stale as service performance changes — revisit budgets quarterly.
|
|
608
|
+
|
|
609
|
+
### Timeout Anti-Patterns
|
|
610
|
+
|
|
611
|
+
**No timeout set:** The default for most HTTP clients is no timeout. An unresponsive downstream service holds a connection open indefinitely, exhausting the upstream's connection pool.
|
|
612
|
+
|
|
613
|
+
**Identical timeout across all tiers:** Setting 5s everywhere means a chain of 5 services each with 5s timeouts can take 25s to fail. Use smaller timeouts deeper in the call graph.
|
|
614
|
+
|
|
615
|
+
**Timeout without circuit breaker:** After the timeout threshold, retries still hit the failing service. Add a circuit breaker so repeated timeouts open the circuit and stop sending requests.
|
|
616
|
+
|
|
617
|
+
## Common Pitfalls
|
|
618
|
+
|
|
619
|
+
**Version drift without sunset enforcement.** Running v1, v2, and v3 simultaneously with no committed sunset dates. Consumers never migrate because the old version keeps working. Fix: enforce sunset dates in CI (fail the build if a past-sunset version is still active).
|
|
620
|
+
|
|
621
|
+
**Retrying non-idempotent POSTs.** Automatic retry on a payment POST without an idempotency key causes duplicate charges. Fix: require idempotency keys on all mutation endpoints; only retry when the key is present.
|
|
622
|
+
|
|
623
|
+
**Jitter-free exponential backoff.** All clients retry at exactly 100ms, 200ms, 400ms — synchronized thundering herd that overwhelms the recovering service. Fix: add full jitter.
|
|
624
|
+
|
|
625
|
+
**Missing consumer contract tests.** A provider changes a response field name. No tests catch it. Consumer breaks in production. Fix: implement consumer-driven contract tests (Pact) that run in the provider's CI pipeline.
|
|
626
|
+
|
|
627
|
+
**Timeout too high on outer layer, too low on inner layer.** Inner services time out before the outer caller does. Callers retry, inner service is already overloaded. Fix: use deadline propagation so all layers share one absolute deadline.
|
|
628
|
+
|
|
629
|
+
## See Also
|
|
630
|
+
|
|
631
|
+
- [multi-service-architecture](./multi-service-architecture.md) — Service boundary design and communication patterns
|
|
632
|
+
- [multi-service-auth](./multi-service-auth.md) — mTLS, service tokens, and zero-trust for inter-service calls
|
|
633
|
+
- [api-design](./api-design.md) — REST and GraphQL design principles
|
|
634
|
+
- [testing-strategy](./testing-strategy.md) — Contract testing with Pact
|