@k2works/claude-code-booster 3.4.1 → 3.6.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/LICENSE +21 -21
- package/README.md +42 -42
- package/bin/claude-code-booster +90 -90
- package/lib/assets/.claude/README.md +239 -239
- package/lib/assets/.claude/scripts/generate-inception-deck.mjs +911 -911
- package/lib/assets/.claude/settings.json +11 -11
- package/lib/assets/.claude/skills/ai-agent-guidelines/SKILL.md +111 -111
- package/lib/assets/.claude/skills/analyzing-architecture/SKILL.md +83 -83
- package/lib/assets/.claude/skills/analyzing-business/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-data-model/SKILL.md +77 -77
- package/lib/assets/.claude/skills/analyzing-domain-model/SKILL.md +117 -88
- package/lib/assets/.claude/skills/analyzing-inception-deck/SKILL.md +84 -84
- package/lib/assets/.claude/skills/analyzing-non-functional/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-operation/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-requirements/SKILL.md +91 -91
- package/lib/assets/.claude/skills/analyzing-tech-stack/SKILL.md +101 -101
- package/lib/assets/.claude/skills/analyzing-test-strategy/SKILL.md +89 -89
- package/lib/assets/.claude/skills/analyzing-ui-design/SKILL.md +80 -80
- package/lib/assets/.claude/skills/analyzing-usecases/SKILL.md +72 -72
- package/lib/assets/.claude/skills/creating-adr/SKILL.md +113 -113
- package/lib/assets/.claude/skills/developing-backend/SKILL.md +100 -100
- package/lib/assets/.claude/skills/developing-frontend/SKILL.md +93 -93
- package/lib/assets/.claude/skills/developing-release/SKILL.md +120 -120
- package/lib/assets/.claude/skills/generating-slides/SKILL.md +94 -94
- package/lib/assets/.claude/skills/git-commit/SKILL.md +81 -81
- package/lib/assets/.claude/skills/killing-processes/SKILL.md +44 -44
- package/lib/assets/.claude/skills/operating-backup/SKILL.md +59 -59
- package/lib/assets/.claude/skills/operating-cicd/SKILL.md +54 -54
- package/lib/assets/.claude/skills/operating-deploy/SKILL.md +67 -67
- package/lib/assets/.claude/skills/operating-docs/SKILL.md +219 -219
- package/lib/assets/.claude/skills/operating-provision/SKILL.md +77 -77
- package/lib/assets/.claude/skills/operating-setup/SKILL.md +63 -63
- package/lib/assets/.claude/skills/orchestrating-analysis/SKILL.md +104 -104
- package/lib/assets/.claude/skills/orchestrating-development/SKILL.md +162 -161
- package/lib/assets/.claude/skills/orchestrating-operation/SKILL.md +158 -158
- package/lib/assets/.claude/skills/orchestrating-project/SKILL.md +144 -144
- package/lib/assets/.claude/skills/planning-releases/SKILL.md +119 -119
- package/lib/assets/.claude/skills/syncing-github-project/SKILL.md +151 -151
- package/lib/assets/.claude/skills/tracking-progress/SKILL.md +91 -91
- package/lib/assets/.claude/skills/validating-iteration-plan/SKILL.md +29 -1
- package/lib/assets/.devcontainer/devcontainer.json +34 -34
- package/lib/assets/.env.example +17 -17
- package/lib/assets/.gitattributes +4 -4
- package/lib/assets/.github/workflows/docker-publish.yml +77 -77
- package/lib/assets/.github/workflows/mkdocs.yml +39 -39
- package/lib/assets/AGENTS.md +94 -94
- package/lib/assets/CLAUDE.md +183 -183
- package/lib/assets/README.md +254 -254
- package/lib/assets/docker-compose.yml +33 -33
- package/lib/assets/docs/adr/index.md +10 -10
- package/lib/assets/docs/article/functional-desgin-ppp/all/01-immutability-and-data-transformation.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/all/02-function-composition.md +519 -519
- package/lib/assets/docs/article/functional-desgin-ppp/all/03-polymorphism.md +537 -537
- package/lib/assets/docs/article/functional-desgin-ppp/all/04-data-validation.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/all/05-property-based-testing.md +320 -320
- package/lib/assets/docs/article/functional-desgin-ppp/all/06-tdd-and-functional.md +498 -498
- package/lib/assets/docs/article/functional-desgin-ppp/all/07-composite-pattern.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/all/08-decorator-pattern.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/all/09-adapter-pattern.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/all/10-strategy-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/all/11-command-pattern.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/12-visitor-pattern.md +322 -322
- package/lib/assets/docs/article/functional-desgin-ppp/all/13-abstract-factory-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/all/14-abstract-server-pattern.md +365 -365
- package/lib/assets/docs/article/functional-desgin-ppp/all/15-gossiping-bus-drivers.md +156 -156
- package/lib/assets/docs/article/functional-desgin-ppp/all/16-payroll-system.md +178 -178
- package/lib/assets/docs/article/functional-desgin-ppp/all/17-video-rental-system.md +312 -312
- package/lib/assets/docs/article/functional-desgin-ppp/all/18-concurrency-system.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/all/19-wa-tor-simulation.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/20-pattern-interactions.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/all/21-best-practices.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/all/22-oo-to-fp-migration.md +337 -337
- package/lib/assets/docs/article/functional-desgin-ppp/all/index.md +388 -388
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/01-immutability-and-data-transformation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/02-function-composition.md +380 -380
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/03-polymorphism.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/04-clojure-spec.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/05-property-based-testing.md +352 -352
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/06-tdd-in-functional.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/07-composite-pattern.md +529 -529
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/08-decorator-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/09-adapter-pattern.md +399 -399
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/10-strategy-pattern.md +485 -485
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/11-command-pattern.md +566 -566
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/12-visitor-pattern.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/13-abstract-factory-pattern.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/14-abstract-server-pattern.md +462 -462
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/15-gossiping-bus-drivers.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/16-payroll-system.md +401 -401
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/17-video-rental-system.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/18-concurrency-system.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/19-wator-simulation.md +739 -739
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/20-pattern-interactions.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/21-best-practices.md +518 -518
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/22-oo-to-fp-migration.md +532 -532
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/index.md +241 -241
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/01-immutability-and-data-transformation.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/02-function-composition.md +374 -374
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/03-polymorphism.md +375 -375
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/04-data-validation.md +195 -195
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/05-property-based-testing.md +268 -268
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/06-tdd-and-fp.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/07-effects-and-pure-functions.md +164 -164
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/08-error-handling-strategies.md +168 -168
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/09-io-and-external-systems.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/10-concurrency-patterns.md +269 -269
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/11-command-pattern.md +148 -148
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/12-visitor-pattern.md +176 -176
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/13-abstract-factory-pattern.md +604 -604
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/14-abstract-server-pattern.md +729 -729
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/15-gossiping-bus-drivers.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/16-payroll-system.md +420 -420
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/17-video-rental-system.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/18-concurrency-system.md +466 -466
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/19-wator-simulation.md +523 -523
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/20-pattern-interactions.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/21-best-practices.md +340 -340
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/22-oo-to-fp-migration.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/index.md +248 -248
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/01-immutability-and-data-transformation.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/02-function-composition.md +452 -452
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/03-polymorphism.md +495 -495
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/04-data-validation.md +416 -416
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/05-property-based-testing.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/06-tdd-functional.md +687 -687
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/07-composite-pattern.md +442 -442
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/08-decorator-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/09-adapter-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/10-strategy-pattern.md +427 -427
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/11-command-pattern.md +428 -428
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/12-visitor-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/13-abstract-factory-pattern.md +309 -309
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/14-abstract-server-pattern.md +596 -596
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/15-gossiping-bus-drivers.md +355 -355
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/16-payroll-system.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/17-video-rental-system.md +414 -414
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/18-concurrency-system.md +367 -367
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/19-wator-simulation.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/20-pattern-interactions.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/21-best-practices.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/22-oo-to-fp-migration.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/index.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/01-immutability-and-data-transformation.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/02-function-composition.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/03-polymorphism.md +362 -362
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/04-data-validation.md +257 -257
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/05-property-based-testing.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/06-tdd-functional.md +283 -283
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/07-composite-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/08-decorator-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/09-adapter-pattern.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/10-strategy-pattern.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/11-command-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/12-visitor-pattern.md +326 -326
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/13-abstract-factory-pattern.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/14-abstract-server-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/15-gossiping-bus-drivers.md +177 -177
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/16-payroll-system.md +219 -219
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/17-video-rental-system.md +244 -244
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/18-concurrency-system.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/19-wator-simulation.md +438 -438
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/20-pattern-interactions.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/21-best-practices.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/22-oo-to-fp-migration.md +469 -469
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/index.md +174 -174
- package/lib/assets/docs/article/functional-desgin-ppp/index.md +90 -90
- package/lib/assets/docs/article/functional-desgin-ppp/rust/01-immutability-and-data-transformation.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/rust/02-function-composition.md +463 -463
- package/lib/assets/docs/article/functional-desgin-ppp/rust/03-polymorphism.md +425 -425
- package/lib/assets/docs/article/functional-desgin-ppp/rust/04-data-validation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/rust/05-property-based-testing.md +247 -247
- package/lib/assets/docs/article/functional-desgin-ppp/rust/06-tdd-and-functional.md +841 -841
- package/lib/assets/docs/article/functional-desgin-ppp/rust/07-composite-pattern.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/rust/08-decorator-pattern.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/rust/09-adapter-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/rust/10-strategy-pattern.md +331 -331
- package/lib/assets/docs/article/functional-desgin-ppp/rust/11-command-pattern.md +356 -356
- package/lib/assets/docs/article/functional-desgin-ppp/rust/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/rust/13-abstract-factory-pattern.md +361 -361
- package/lib/assets/docs/article/functional-desgin-ppp/rust/14-abstract-server-pattern.md +392 -392
- package/lib/assets/docs/article/functional-desgin-ppp/rust/15-gossiping-bus-drivers.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/rust/16-payroll-system.md +297 -297
- package/lib/assets/docs/article/functional-desgin-ppp/rust/17-video-rental-system.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/18-concurrency-system.md +315 -315
- package/lib/assets/docs/article/functional-desgin-ppp/rust/19-wator-simulation.md +311 -311
- package/lib/assets/docs/article/functional-desgin-ppp/rust/20-pattern-interactions.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/21-best-practices.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/rust/22-oo-to-fp-migration.md +349 -349
- package/lib/assets/docs/article/functional-desgin-ppp/rust/index.md +243 -243
- package/lib/assets/docs/article/functional-desgin-ppp/scala/01-immutability-and-data-transformation.md +328 -328
- package/lib/assets/docs/article/functional-desgin-ppp/scala/02-function-composition.md +348 -348
- package/lib/assets/docs/article/functional-desgin-ppp/scala/03-polymorphism.md +357 -357
- package/lib/assets/docs/article/functional-desgin-ppp/scala/04-data-validation.md +364 -364
- package/lib/assets/docs/article/functional-desgin-ppp/scala/05-property-based-testing.md +515 -515
- package/lib/assets/docs/article/functional-desgin-ppp/scala/06-tdd-functional.md +557 -557
- package/lib/assets/docs/article/functional-desgin-ppp/scala/07-composite-pattern.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/scala/08-decorator-pattern.md +327 -327
- package/lib/assets/docs/article/functional-desgin-ppp/scala/09-adapter-pattern.md +517 -517
- package/lib/assets/docs/article/functional-desgin-ppp/scala/10-strategy-pattern.md +441 -441
- package/lib/assets/docs/article/functional-desgin-ppp/scala/11-command-pattern.md +407 -407
- package/lib/assets/docs/article/functional-desgin-ppp/scala/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/scala/13-abstract-factory-pattern.md +398 -398
- package/lib/assets/docs/article/functional-desgin-ppp/scala/14-abstract-server-pattern.md +476 -476
- package/lib/assets/docs/article/functional-desgin-ppp/scala/15-gossiping-bus-drivers.md +391 -391
- package/lib/assets/docs/article/functional-desgin-ppp/scala/16-payroll-system.md +342 -342
- package/lib/assets/docs/article/functional-desgin-ppp/scala/17-video-rental-system.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/scala/18-concurrency-system.md +730 -730
- package/lib/assets/docs/article/functional-desgin-ppp/scala/19-wator-simulation.md +624 -624
- package/lib/assets/docs/article/functional-desgin-ppp/scala/20-pattern-interactions.md +512 -512
- package/lib/assets/docs/article/functional-desgin-ppp/scala/21-best-practices.md +433 -433
- package/lib/assets/docs/article/functional-desgin-ppp/scala/22-oo-to-fp-migration.md +688 -688
- package/lib/assets/docs/article/functional-desgin-ppp/scala/index.md +243 -243
- package/lib/assets/docs/article/getting-start-tdd/clojure/01-todo-list-and-first-test.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/clojure/02-fake-it-and-triangulation.md +162 -162
- package/lib/assets/docs/article/getting-start-tdd/clojure/03-obvious-implementation-and-refactoring.md +135 -135
- package/lib/assets/docs/article/getting-start-tdd/clojure/04-version-control-and-conventional-commits.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/clojure/05-package-management-and-static-analysis.md +299 -299
- package/lib/assets/docs/article/getting-start-tdd/clojure/06-task-runner-and-ci-cd.md +241 -241
- package/lib/assets/docs/article/getting-start-tdd/clojure/07-protocols-and-records.md +131 -131
- package/lib/assets/docs/article/getting-start-tdd/clojure/08-multimethods-and-design-patterns.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/clojure/09-namespaces-and-module-design.md +127 -127
- package/lib/assets/docs/article/getting-start-tdd/clojure/10-higher-order-functions-and-composition.md +114 -114
- package/lib/assets/docs/article/getting-start-tdd/clojure/11-persistent-data-and-pipeline.md +138 -138
- package/lib/assets/docs/article/getting-start-tdd/clojure/12-error-handling-and-spec.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/clojure/index.md +65 -65
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter01.md +232 -232
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter02.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter03.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter04.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter05.md +256 -256
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter06.md +195 -195
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter07.md +214 -214
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter08.md +249 -249
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter09.md +174 -174
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter10.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter11.md +192 -192
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter12.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/csharp/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/elixir/01-todo-list-and-first-test.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/elixir/02-fake-it-and-triangulation.md +95 -95
- package/lib/assets/docs/article/getting-start-tdd/elixir/03-obvious-implementation-and-refactoring.md +109 -109
- package/lib/assets/docs/article/getting-start-tdd/elixir/04-version-control-and-conventional-commits.md +96 -96
- package/lib/assets/docs/article/getting-start-tdd/elixir/05-package-management-and-static-analysis.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/elixir/06-task-runner-and-ci-cd.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/elixir/07-structs-and-protocols.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/elixir/08-pattern-matching-and-guards.md +108 -108
- package/lib/assets/docs/article/getting-start-tdd/elixir/09-module-design-and-behaviours.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/elixir/10-higher-order-functions-and-pipeline.md +178 -178
- package/lib/assets/docs/article/getting-start-tdd/elixir/11-stream-and-lazy-evaluation.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/elixir/12-error-handling-and-with.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/elixir/index.md +35 -35
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter01.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter02.md +246 -246
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter03.md +218 -218
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter04.md +179 -179
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter05.md +267 -267
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter06.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter07.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter08.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter09.md +222 -222
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter10.md +189 -189
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter11.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter12.md +215 -215
- package/lib/assets/docs/article/getting-start-tdd/fsharp/index.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/go/01-todo-list-and-first-test.md +213 -213
- package/lib/assets/docs/article/getting-start-tdd/go/02-fake-it-and-triangulation.md +302 -302
- package/lib/assets/docs/article/getting-start-tdd/go/03-obvious-implementation-and-refactoring.md +339 -339
- package/lib/assets/docs/article/getting-start-tdd/go/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/go/05-package-management-and-static-analysis.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/go/06-task-runner-and-ci-cd.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/go/07-encapsulation-and-polymorphism.md +394 -394
- package/lib/assets/docs/article/getting-start-tdd/go/08-design-patterns.md +422 -422
- package/lib/assets/docs/article/getting-start-tdd/go/09-solid-principles-and-module-design.md +400 -400
- package/lib/assets/docs/article/getting-start-tdd/go/10-higher-order-functions-and-composition.md +226 -226
- package/lib/assets/docs/article/getting-start-tdd/go/11-immutable-data-and-pipeline.md +296 -296
- package/lib/assets/docs/article/getting-start-tdd/go/12-error-handling-and-type-safety.md +411 -411
- package/lib/assets/docs/article/getting-start-tdd/go/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/haskell/01-todo-list-and-first-test.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/haskell/02-fake-it-and-triangulation.md +337 -337
- package/lib/assets/docs/article/getting-start-tdd/haskell/03-obvious-implementation-and-refactoring.md +257 -257
- package/lib/assets/docs/article/getting-start-tdd/haskell/04-version-control-and-conventional-commits.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/haskell/05-package-management-and-static-analysis.md +313 -313
- package/lib/assets/docs/article/getting-start-tdd/haskell/06-task-runner-and-ci-cd.md +309 -309
- package/lib/assets/docs/article/getting-start-tdd/haskell/07-algebraic-data-types-and-type-classes.md +412 -412
- package/lib/assets/docs/article/getting-start-tdd/haskell/08-pattern-matching-and-guards.md +390 -390
- package/lib/assets/docs/article/getting-start-tdd/haskell/09-module-design-and-smart-constructors.md +461 -461
- package/lib/assets/docs/article/getting-start-tdd/haskell/10-higher-order-functions-and-currying.md +434 -434
- package/lib/assets/docs/article/getting-start-tdd/haskell/11-function-composition-and-point-free.md +392 -392
- package/lib/assets/docs/article/getting-start-tdd/haskell/12-monad-and-error-handling.md +631 -631
- package/lib/assets/docs/article/getting-start-tdd/haskell/index.md +49 -49
- package/lib/assets/docs/article/getting-start-tdd/index.md +93 -93
- package/lib/assets/docs/article/getting-start-tdd/integration/01-language-overview.md +375 -375
- package/lib/assets/docs/article/getting-start-tdd/integration/02-test-framework-comparison.md +349 -349
- package/lib/assets/docs/article/getting-start-tdd/integration/03-tdd-pattern-comparison.md +445 -445
- package/lib/assets/docs/article/getting-start-tdd/integration/04-type-system-comparison.md +409 -409
- package/lib/assets/docs/article/getting-start-tdd/integration/05-dev-environment-comparison.md +330 -330
- package/lib/assets/docs/article/getting-start-tdd/integration/06-learning-roadmap.md +290 -290
- package/lib/assets/docs/article/getting-start-tdd/integration/index.md +69 -69
- package/lib/assets/docs/article/getting-start-tdd/java/01-todo-list-and-first-test.md +234 -234
- package/lib/assets/docs/article/getting-start-tdd/java/02-fake-it-and-triangulation.md +261 -261
- package/lib/assets/docs/article/getting-start-tdd/java/03-obvious-implementation-and-refactoring.md +185 -185
- package/lib/assets/docs/article/getting-start-tdd/java/04-version-control-and-conventional-commits.md +115 -115
- package/lib/assets/docs/article/getting-start-tdd/java/05-package-management-and-static-analysis.md +382 -382
- package/lib/assets/docs/article/getting-start-tdd/java/06-task-runner-and-ci-cd.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/java/07-encapsulation-and-polymorphism.md +626 -626
- package/lib/assets/docs/article/getting-start-tdd/java/08-design-patterns.md +393 -393
- package/lib/assets/docs/article/getting-start-tdd/java/09-solid-principles-and-module-design.md +310 -310
- package/lib/assets/docs/article/getting-start-tdd/java/10-higher-order-functions-and-composition.md +188 -188
- package/lib/assets/docs/article/getting-start-tdd/java/11-immutable-data-and-pipeline.md +167 -167
- package/lib/assets/docs/article/getting-start-tdd/java/12-error-handling-and-type-safety.md +205 -205
- package/lib/assets/docs/article/getting-start-tdd/java/index.md +61 -61
- package/lib/assets/docs/article/getting-start-tdd/node/01-todo-list-and-first-test.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/node/02-fake-it-and-triangulation.md +262 -262
- package/lib/assets/docs/article/getting-start-tdd/node/03-obvious-implementation-and-refactoring.md +169 -169
- package/lib/assets/docs/article/getting-start-tdd/node/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/node/05-package-management-and-static-analysis.md +314 -314
- package/lib/assets/docs/article/getting-start-tdd/node/06-task-runner-and-ci-cd.md +235 -235
- package/lib/assets/docs/article/getting-start-tdd/node/07-encapsulation-and-polymorphism.md +327 -327
- package/lib/assets/docs/article/getting-start-tdd/node/08-design-patterns.md +322 -322
- package/lib/assets/docs/article/getting-start-tdd/node/09-solid-principles-and-module-design.md +285 -285
- package/lib/assets/docs/article/getting-start-tdd/node/10-higher-order-functions-and-composition.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/node/11-immutable-data-and-pipeline.md +207 -207
- package/lib/assets/docs/article/getting-start-tdd/node/12-error-handling-and-type-safety.md +295 -295
- package/lib/assets/docs/article/getting-start-tdd/node/index.md +56 -56
- package/lib/assets/docs/article/getting-start-tdd/php/01-todo-list-and-first-test.md +259 -259
- package/lib/assets/docs/article/getting-start-tdd/php/02-fake-it-and-triangulation.md +200 -200
- package/lib/assets/docs/article/getting-start-tdd/php/03-obvious-implementation-and-refactoring.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/php/04-version-control-and-conventional-commits.md +141 -141
- package/lib/assets/docs/article/getting-start-tdd/php/05-package-management-and-static-analysis.md +410 -410
- package/lib/assets/docs/article/getting-start-tdd/php/06-task-runner-and-ci-cd.md +321 -321
- package/lib/assets/docs/article/getting-start-tdd/php/07-encapsulation-and-polymorphism.md +372 -372
- package/lib/assets/docs/article/getting-start-tdd/php/08-design-patterns.md +453 -453
- package/lib/assets/docs/article/getting-start-tdd/php/09-solid-principles-and-module-design.md +460 -460
- package/lib/assets/docs/article/getting-start-tdd/php/10-higher-order-functions-and-composition.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/php/11-immutable-data-and-pipeline.md +266 -266
- package/lib/assets/docs/article/getting-start-tdd/php/12-error-handling-and-type-safety.md +308 -308
- package/lib/assets/docs/article/getting-start-tdd/php/index.md +84 -84
- package/lib/assets/docs/article/getting-start-tdd/python/01-todo-list-and-first-test.md +201 -201
- package/lib/assets/docs/article/getting-start-tdd/python/02-fake-it-and-triangulation.md +247 -247
- package/lib/assets/docs/article/getting-start-tdd/python/03-obvious-implementation-and-refactoring.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/python/04-version-control-and-conventional-commits.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/python/05-package-management-and-static-analysis.md +274 -274
- package/lib/assets/docs/article/getting-start-tdd/python/06-task-runner-and-ci-cd.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/python/07-encapsulation-and-polymorphism.md +208 -208
- package/lib/assets/docs/article/getting-start-tdd/python/08-design-patterns.md +172 -172
- package/lib/assets/docs/article/getting-start-tdd/python/09-solid-principles-and-module-design.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/python/10-higher-order-functions-and-composition.md +122 -122
- package/lib/assets/docs/article/getting-start-tdd/python/11-immutable-data-and-pipeline.md +116 -116
- package/lib/assets/docs/article/getting-start-tdd/python/12-error-handling-and-type-safety.md +126 -126
- package/lib/assets/docs/article/getting-start-tdd/python/index.md +55 -55
- package/lib/assets/docs/article/getting-start-tdd/ruby/01-todo-list-and-first-test.md +231 -231
- package/lib/assets/docs/article/getting-start-tdd/ruby/02-fake-it-and-triangulation.md +238 -238
- package/lib/assets/docs/article/getting-start-tdd/ruby/03-obvious-implementation-and-refactoring.md +228 -228
- package/lib/assets/docs/article/getting-start-tdd/ruby/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/ruby/05-package-management-and-static-analysis.md +287 -287
- package/lib/assets/docs/article/getting-start-tdd/ruby/06-task-runner-and-ci-cd.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/ruby/07-encapsulation-and-polymorphism.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/ruby/08-design-patterns.md +329 -329
- package/lib/assets/docs/article/getting-start-tdd/ruby/09-solid-principles-and-module-design.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/ruby/10-higher-order-functions-and-composition.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/ruby/11-immutable-data-and-pipeline.md +237 -237
- package/lib/assets/docs/article/getting-start-tdd/ruby/12-error-handling-and-type-safety.md +398 -398
- package/lib/assets/docs/article/getting-start-tdd/ruby/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/rust/01-todo-list-and-first-test.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/rust/02-fake-it-and-triangulation.md +264 -264
- package/lib/assets/docs/article/getting-start-tdd/rust/03-obvious-implementation-and-refactoring.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/rust/04-version-control-and-conventional-commits.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/rust/05-package-management-and-static-analysis.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/rust/06-task-runner-and-ci-cd.md +164 -164
- package/lib/assets/docs/article/getting-start-tdd/rust/07-encapsulation-and-polymorphism.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/rust/08-design-patterns.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/rust/09-solid-principles-and-module-design.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/rust/10-higher-order-functions-and-composition.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/rust/11-immutable-data-and-pipeline.md +105 -105
- package/lib/assets/docs/article/getting-start-tdd/rust/12-error-handling-and-type-safety.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/rust/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/scala/01-todo-list-and-first-test.md +111 -111
- package/lib/assets/docs/article/getting-start-tdd/scala/02-fake-it-and-triangulation.md +107 -107
- package/lib/assets/docs/article/getting-start-tdd/scala/03-obvious-implementation-and-refactoring.md +99 -99
- package/lib/assets/docs/article/getting-start-tdd/scala/04-version-control-and-conventional-commits.md +123 -123
- package/lib/assets/docs/article/getting-start-tdd/scala/05-package-management-and-static-analysis.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/scala/06-task-runner-and-ci-cd.md +186 -186
- package/lib/assets/docs/article/getting-start-tdd/scala/07-case-classes-and-traits.md +139 -139
- package/lib/assets/docs/article/getting-start-tdd/scala/08-pattern-matching-and-sealed-traits.md +106 -106
- package/lib/assets/docs/article/getting-start-tdd/scala/09-packages-and-module-design.md +75 -75
- package/lib/assets/docs/article/getting-start-tdd/scala/10-higher-order-functions-and-composition.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/scala/11-collections-and-lazy-evaluation.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/scala/12-error-handling-and-type-safety.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/scala/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/all/index.md +404 -404
- package/lib/assets/docs/article/grokking-concurrency/all/part-1-ch02-sequential.md +554 -554
- package/lib/assets/docs/article/grokking-concurrency/all/part-2-ch04-05-threads.md +469 -469
- package/lib/assets/docs/article/grokking-concurrency/all/part-3-ch06-multitasking.md +520 -520
- package/lib/assets/docs/article/grokking-concurrency/all/part-4-ch07-parallel-patterns.md +420 -420
- package/lib/assets/docs/article/grokking-concurrency/all/part-5-ch08-09-synchronization.md +510 -510
- package/lib/assets/docs/article/grokking-concurrency/all/part-6-ch10-11-nonblocking-io.md +435 -435
- package/lib/assets/docs/article/grokking-concurrency/all/part-7-ch12-async.md +465 -465
- package/lib/assets/docs/article/grokking-concurrency/all/part-8-ch13-mapreduce.md +377 -377
- package/lib/assets/docs/article/grokking-concurrency/clojure/index.md +116 -116
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-2.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-3.md +122 -122
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-4.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-5.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-6.md +89 -89
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-7.md +100 -100
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-8.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/csharp/index.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-1.md +97 -97
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-2.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-3.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-4.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-6.md +61 -61
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-7.md +84 -84
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-8.md +92 -92
- package/lib/assets/docs/article/grokking-concurrency/fsharp/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-1.md +80 -80
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-2.md +103 -103
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-3.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-5.md +104 -104
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-6.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-7.md +121 -121
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-8.md +107 -107
- package/lib/assets/docs/article/grokking-concurrency/haskell/index.md +248 -248
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-1.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-2.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-3.md +91 -91
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-4.md +106 -106
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-6.md +95 -95
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-7.md +111 -111
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-8.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/index.md +66 -66
- package/lib/assets/docs/article/grokking-concurrency/java/index.md +102 -102
- package/lib/assets/docs/article/grokking-concurrency/java/part-1.md +308 -308
- package/lib/assets/docs/article/grokking-concurrency/java/part-2.md +334 -334
- package/lib/assets/docs/article/grokking-concurrency/java/part-3.md +221 -221
- package/lib/assets/docs/article/grokking-concurrency/java/part-4.md +213 -213
- package/lib/assets/docs/article/grokking-concurrency/java/part-5.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/java/part-6.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/java/part-7.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/java/part-8.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/python/index.md +313 -313
- package/lib/assets/docs/article/grokking-concurrency/python/part-1.md +239 -239
- package/lib/assets/docs/article/grokking-concurrency/python/part-2.md +418 -418
- package/lib/assets/docs/article/grokking-concurrency/python/part-3.md +227 -227
- package/lib/assets/docs/article/grokking-concurrency/python/part-4.md +299 -299
- package/lib/assets/docs/article/grokking-concurrency/python/part-5.md +315 -315
- package/lib/assets/docs/article/grokking-concurrency/python/part-6.md +297 -297
- package/lib/assets/docs/article/grokking-concurrency/python/part-7.md +314 -314
- package/lib/assets/docs/article/grokking-concurrency/python/part-8.md +360 -360
- package/lib/assets/docs/article/grokking-concurrency/rust/index.md +270 -270
- package/lib/assets/docs/article/grokking-concurrency/rust/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/rust/part-2.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/rust/part-3.md +126 -126
- package/lib/assets/docs/article/grokking-concurrency/rust/part-4.md +175 -175
- package/lib/assets/docs/article/grokking-concurrency/rust/part-5.md +158 -158
- package/lib/assets/docs/article/grokking-concurrency/rust/part-6.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/rust/part-7.md +133 -133
- package/lib/assets/docs/article/grokking-concurrency/rust/part-8.md +155 -155
- package/lib/assets/docs/article/grokking-concurrency/scala/index.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/scala/part-1.md +78 -78
- package/lib/assets/docs/article/grokking-concurrency/scala/part-2.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/scala/part-3.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/scala/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/scala/part-5.md +119 -119
- package/lib/assets/docs/article/grokking-concurrency/scala/part-6.md +83 -83
- package/lib/assets/docs/article/grokking-concurrency/scala/part-7.md +131 -131
- package/lib/assets/docs/article/grokking-concurrency/scala/part-8.md +129 -129
- package/lib/assets/docs/article/grokkingfp/all/index.md +368 -368
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch01-fp-introduction.md +530 -530
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch02-pure-functions.md +923 -923
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch03-immutable-data.md +1128 -1128
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch04-higher-order-functions.md +1104 -1104
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch05-flatmap.md +1026 -1026
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch06-option.md +785 -785
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch07-either-adt.md +871 -871
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch08-io-monad.md +972 -972
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch09-streams.md +926 -926
- package/lib/assets/docs/article/grokkingfp/all/part-5-ch10-concurrency.md +870 -870
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch11-application.md +715 -715
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch12-testing.md +626 -626
- package/lib/assets/docs/article/grokkingfp/all/writing-plan.md +712 -712
- package/lib/assets/docs/article/grokkingfp/clojure/index.md +276 -276
- package/lib/assets/docs/article/grokkingfp/clojure/part-1.md +667 -667
- package/lib/assets/docs/article/grokkingfp/clojure/part-2.md +643 -643
- package/lib/assets/docs/article/grokkingfp/clojure/part-3.md +620 -620
- package/lib/assets/docs/article/grokkingfp/clojure/part-4.md +697 -697
- package/lib/assets/docs/article/grokkingfp/clojure/part-5.md +751 -751
- package/lib/assets/docs/article/grokkingfp/clojure/part-6.md +721 -721
- package/lib/assets/docs/article/grokkingfp/csharp/index.md +246 -246
- package/lib/assets/docs/article/grokkingfp/csharp/part-1.md +811 -811
- package/lib/assets/docs/article/grokkingfp/csharp/part-2.md +971 -971
- package/lib/assets/docs/article/grokkingfp/csharp/part-3.md +981 -981
- package/lib/assets/docs/article/grokkingfp/csharp/part-4.md +949 -949
- package/lib/assets/docs/article/grokkingfp/csharp/part-5.md +947 -947
- package/lib/assets/docs/article/grokkingfp/csharp/part-6.md +739 -739
- package/lib/assets/docs/article/grokkingfp/elixir/index.md +203 -203
- package/lib/assets/docs/article/grokkingfp/elixir/part-1.md +712 -712
- package/lib/assets/docs/article/grokkingfp/elixir/part-2.md +838 -838
- package/lib/assets/docs/article/grokkingfp/elixir/part-3.md +985 -985
- package/lib/assets/docs/article/grokkingfp/elixir/part-4.md +974 -974
- package/lib/assets/docs/article/grokkingfp/elixir/part-5.md +1286 -1286
- package/lib/assets/docs/article/grokkingfp/elixir/part-6.md +1049 -1049
- package/lib/assets/docs/article/grokkingfp/fsharp/index.md +210 -210
- package/lib/assets/docs/article/grokkingfp/fsharp/part-1.md +714 -714
- package/lib/assets/docs/article/grokkingfp/fsharp/part-2.md +961 -961
- package/lib/assets/docs/article/grokkingfp/fsharp/part-3.md +972 -972
- package/lib/assets/docs/article/grokkingfp/fsharp/part-4.md +832 -832
- package/lib/assets/docs/article/grokkingfp/fsharp/part-5.md +911 -911
- package/lib/assets/docs/article/grokkingfp/fsharp/part-6.md +922 -922
- package/lib/assets/docs/article/grokkingfp/haskell/index.md +234 -234
- package/lib/assets/docs/article/grokkingfp/haskell/part-1.md +591 -591
- package/lib/assets/docs/article/grokkingfp/haskell/part-2.md +866 -866
- package/lib/assets/docs/article/grokkingfp/haskell/part-3.md +915 -915
- package/lib/assets/docs/article/grokkingfp/haskell/part-4.md +878 -878
- package/lib/assets/docs/article/grokkingfp/haskell/part-5.md +845 -845
- package/lib/assets/docs/article/grokkingfp/haskell/part-6.md +844 -844
- package/lib/assets/docs/article/grokkingfp/index.md +143 -143
- package/lib/assets/docs/article/grokkingfp/java/index.md +211 -211
- package/lib/assets/docs/article/grokkingfp/java/part-1.md +648 -648
- package/lib/assets/docs/article/grokkingfp/java/part-2.md +675 -675
- package/lib/assets/docs/article/grokkingfp/java/part-3.md +672 -672
- package/lib/assets/docs/article/grokkingfp/java/part-4.md +771 -771
- package/lib/assets/docs/article/grokkingfp/java/part-5.md +959 -959
- package/lib/assets/docs/article/grokkingfp/java/part-6.md +1328 -1328
- package/lib/assets/docs/article/grokkingfp/python/index.md +258 -258
- package/lib/assets/docs/article/grokkingfp/python/part-1.md +443 -443
- package/lib/assets/docs/article/grokkingfp/python/part-2.md +958 -958
- package/lib/assets/docs/article/grokkingfp/python/part-3.md +1004 -1004
- package/lib/assets/docs/article/grokkingfp/python/part-4.md +765 -765
- package/lib/assets/docs/article/grokkingfp/python/part-5.md +747 -747
- package/lib/assets/docs/article/grokkingfp/python/part-6.md +861 -861
- package/lib/assets/docs/article/grokkingfp/ruby/index.md +330 -330
- package/lib/assets/docs/article/grokkingfp/ruby/part-1.md +755 -755
- package/lib/assets/docs/article/grokkingfp/ruby/part-2.md +938 -938
- package/lib/assets/docs/article/grokkingfp/ruby/part-3.md +946 -946
- package/lib/assets/docs/article/grokkingfp/ruby/part-4.md +921 -921
- package/lib/assets/docs/article/grokkingfp/ruby/part-5.md +908 -908
- package/lib/assets/docs/article/grokkingfp/ruby/part-6.md +1412 -1412
- package/lib/assets/docs/article/grokkingfp/rust/index.md +242 -242
- package/lib/assets/docs/article/grokkingfp/rust/part-1.md +634 -634
- package/lib/assets/docs/article/grokkingfp/rust/part-2.md +1060 -1060
- package/lib/assets/docs/article/grokkingfp/rust/part-3.md +994 -994
- package/lib/assets/docs/article/grokkingfp/rust/part-4.md +573 -573
- package/lib/assets/docs/article/grokkingfp/rust/part-5.md +705 -705
- package/lib/assets/docs/article/grokkingfp/rust/part-6.md +508 -508
- package/lib/assets/docs/article/grokkingfp/scala/index.md +171 -171
- package/lib/assets/docs/article/grokkingfp/scala/part-1.md +543 -543
- package/lib/assets/docs/article/grokkingfp/scala/part-2.md +946 -946
- package/lib/assets/docs/article/grokkingfp/scala/part-3.md +919 -919
- package/lib/assets/docs/article/grokkingfp/scala/part-4.md +742 -742
- package/lib/assets/docs/article/grokkingfp/scala/part-5.md +722 -722
- package/lib/assets/docs/article/grokkingfp/scala/part-6.md +867 -867
- package/lib/assets/docs/article/grokkingfp/typescript/index.md +273 -273
- package/lib/assets/docs/article/grokkingfp/typescript/part-1.md +561 -561
- package/lib/assets/docs/article/grokkingfp/typescript/part-2.md +1129 -1129
- package/lib/assets/docs/article/grokkingfp/typescript/part-3.md +842 -842
- package/lib/assets/docs/article/grokkingfp/typescript/part-4.md +1087 -1087
- package/lib/assets/docs/article/grokkingfp/typescript/part-5.md +717 -717
- package/lib/assets/docs/article/grokkingfp/typescript/part-6.md +982 -982
- package/lib/assets/docs/article/practical-database-design/index.md +121 -121
- package/lib/assets/docs/article/practical-database-design/part1/chapter01.md +288 -288
- package/lib/assets/docs/article/practical-database-design/part1/chapter02.md +518 -518
- package/lib/assets/docs/article/practical-database-design/part1/chapter03.md +557 -557
- package/lib/assets/docs/article/practical-database-design/part2/chapter04.md +924 -924
- package/lib/assets/docs/article/practical-database-design/part2/chapter05.md +1627 -1627
- package/lib/assets/docs/article/practical-database-design/part2/chapter06.md +2716 -2716
- package/lib/assets/docs/article/practical-database-design/part2/chapter07.md +2082 -2082
- package/lib/assets/docs/article/practical-database-design/part2/chapter08.md +2105 -2105
- package/lib/assets/docs/article/practical-database-design/part2/chapter09.md +2031 -2031
- package/lib/assets/docs/article/practical-database-design/part2/chapter10.md +1387 -1387
- package/lib/assets/docs/article/practical-database-design/part2/chapter11.md +1677 -1677
- package/lib/assets/docs/article/practical-database-design/part2/chapter12.md +1417 -1417
- package/lib/assets/docs/article/practical-database-design/part2/chapter13.md +1434 -1434
- package/lib/assets/docs/article/practical-database-design/part3/chapter14.md +667 -667
- package/lib/assets/docs/article/practical-database-design/part3/chapter15.md +1625 -1625
- package/lib/assets/docs/article/practical-database-design/part3/chapter16.md +1915 -1915
- package/lib/assets/docs/article/practical-database-design/part3/chapter17.md +1708 -1708
- package/lib/assets/docs/article/practical-database-design/part3/chapter18.md +2095 -2095
- package/lib/assets/docs/article/practical-database-design/part3/chapter19.md +1123 -1123
- package/lib/assets/docs/article/practical-database-design/part3/chapter20.md +1031 -1031
- package/lib/assets/docs/article/practical-database-design/part3/chapter21.md +1382 -1382
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter14-orm.md +991 -991
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter15-orm.md +1300 -1300
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter16-orm.md +1166 -1166
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter17-orm.md +1584 -1584
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter18-orm.md +1183 -1183
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter19-orm.md +1016 -1016
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter20-orm.md +1753 -1753
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter21-orm.md +1447 -1447
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter22-orm.md +1878 -1878
- package/lib/assets/docs/article/practical-database-design/part4/chapter22.md +965 -965
- package/lib/assets/docs/article/practical-database-design/part4/chapter23.md +2069 -2069
- package/lib/assets/docs/article/practical-database-design/part4/chapter24.md +2439 -2439
- package/lib/assets/docs/article/practical-database-design/part4/chapter25.md +3661 -3661
- package/lib/assets/docs/article/practical-database-design/part4/chapter26.md +2916 -2916
- package/lib/assets/docs/article/practical-database-design/part4/chapter27.md +3105 -3105
- package/lib/assets/docs/article/practical-database-design/part4/chapter28.md +2697 -2697
- package/lib/assets/docs/article/practical-database-design/part4/chapter29.md +2544 -2544
- package/lib/assets/docs/article/practical-database-design/part4/chapter30.md +2180 -2180
- package/lib/assets/docs/article/practical-database-design/part4/chapter31.md +1192 -1192
- package/lib/assets/docs/article/practical-database-design/part4/chapter32.md +2101 -2101
- package/lib/assets/docs/article/practical-database-design/part5/chapter33.md +1032 -1032
- package/lib/assets/docs/article/practical-database-design/part5/chapter34.md +1609 -1609
- package/lib/assets/docs/article/practical-database-design/part5/chapter35.md +1453 -1453
- package/lib/assets/docs/article/practical-database-design/part5/chapter36.md +1292 -1292
- package/lib/assets/docs/article/practical-database-design/part5/chapter37.md +1470 -1470
- package/lib/assets/docs/article/practical-database-design/part5/chapter38.md +1698 -1698
- package/lib/assets/docs/article/practical-database-design/part5/chapter39.md +2334 -2334
- package/lib/assets/docs/article/practical-database-design/study/study2-1.md +1693 -1693
- package/lib/assets/docs/article/practical-database-design/study/study2-2.md +1347 -1347
- package/lib/assets/docs/article/practical-database-design/study/study2-3.md +2044 -2044
- package/lib/assets/docs/article/practical-database-design/study/study2-4.md +2229 -2229
- package/lib/assets/docs/article/practical-database-design/study/study2-5.md +2418 -2418
- package/lib/assets/docs/article/practical-database-design/study/study3-1.md +2205 -2205
- package/lib/assets/docs/article/practical-database-design/study/study3-2.md +2221 -2221
- package/lib/assets/docs/article/practical-database-design/study/study3-3.md +2253 -2253
- package/lib/assets/docs/article/practical-database-design/study/study3-4.md +2106 -2106
- package/lib/assets/docs/article/practical-database-design/study/study3-5.md +2507 -2507
- package/lib/assets/docs/article/practical-database-design/study/study4-1.md +2587 -2587
- package/lib/assets/docs/article/practical-database-design/study/study4-2.md +2075 -2075
- package/lib/assets/docs/article/practical-database-design/study/study4-3.md +1805 -1805
- package/lib/assets/docs/article/practical-database-design/study/study4-4.md +1895 -1895
- package/lib/assets/docs/article/practical-database-design/study/study4-5.md +2878 -2878
- package/lib/assets/docs/assets/css/extra.css +29 -29
- package/lib/assets/docs/assets/js/extra.js +44 -44
- package/lib/assets/docs/development/index.md +39 -39
- package/lib/assets/docs/operation/index.md +11 -11
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/343/203/225/343/203/255/343/203/274.md +532 -532
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/265/343/203/274/343/203/220/343/203/274/350/250/255/345/256/232/346/211/213/351/240/206.md +341 -341
- package/lib/assets/docs/reference/Java/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +581 -580
- package/lib/assets/docs/reference/SonarQube/343/203/255/343/203/274/343/202/253/343/203/253/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +642 -642
- package/lib/assets/docs/reference/TypeScript/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +465 -465
- package/lib/assets/docs/reference/UI/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +450 -450
- package/lib/assets/docs/reference/images/Ansoff.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandBasicStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandCategorization.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandRecurutementStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandValue.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BusinessActivitiy.svg +3 -3
- package/lib/assets/docs/reference/images/HRM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/MarketingStructure.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/OrganizationElemnts.svg +3 -3
- package/lib/assets/docs/reference/images/PPM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/PositioningMap.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductLayer.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductMix.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/SWOT.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/TargetMarket.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ThreeGenericStrategies.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/VRIO.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ValueChain.drawio.svg +3 -3
- package/lib/assets/docs/reference/index.md +52 -52
- package/lib/assets/docs/reference//343/202/210/343/201/204/343/202/275/343/203/225/343/203/210/343/202/246/343/202/247/343/202/242/343/201/250/343/201/257.md +250 -242
- package/lib/assets/docs/reference//343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +2216 -2216
- package/lib/assets/docs/reference//343/202/244/343/203/263/343/203/225/343/203/251/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +1878 -1878
- package/lib/assets/docs/reference//343/202/250/343/202/257/343/202/271/343/203/210/343/203/252/343/203/274/343/203/240/343/203/227/343/203/255/343/202/260/343/203/251/343/203/237/343/203/263/343/202/260.md +550 -544
- package/lib/assets/docs/reference//343/202/263/343/203/274/343/203/207/343/202/243/343/203/263/343/202/260/343/201/250/343/203/206/343/202/271/343/203/210/343/202/254/343/202/244/343/203/211.md +705 -705
- package/lib/assets/docs/reference//343/203/206/343/202/271/343/203/210/346/210/246/347/225/245/343/202/254/343/202/244/343/203/211.md +1313 -1313
- package/lib/assets/docs/reference//343/203/207/343/203/274/343/202/277/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +311 -311
- package/lib/assets/docs/reference//343/203/211/343/203/241/343/202/244/343/203/263/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +599 -599
- package/lib/assets/docs/reference//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/345/210/206/346/236/220/343/202/254/343/202/244/343/203/211.md +528 -528
- package/lib/assets/docs/reference//343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +689 -682
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/202/254/343/202/244/343/203/211.md +461 -461
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/203/273/343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/350/250/210/347/224/273/343/202/254/343/202/244/343/203/211.md +580 -560
- package/lib/assets/docs/reference//343/203/255/343/202/270/343/202/253/343/203/253/343/202/267/343/203/263/343/202/255/343/203/263/343/202/260.md +1367 -1367
- package/lib/assets/docs/reference//344/274/201/346/245/255/347/265/214/345/226/266/350/253/226.md +2637 -2636
- package/lib/assets/docs/reference//347/222/260/345/242/203/345/244/211/346/225/260/347/256/241/347/220/206/343/202/254/343/202/244/343/203/211.md +665 -663
- package/lib/assets/docs/reference//350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1248 -1248
- package/lib/assets/docs/reference//350/250/200/350/252/236/345/210/245/351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +28 -0
- package/lib/assets/docs/reference//351/201/213/345/226/266/347/256/241/347/220/206.md +1482 -1482
- package/lib/assets/docs/reference//351/201/213/347/224/250/343/202/271/343/202/257/343/203/252/343/203/227/343/203/210/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +421 -421
- package/lib/assets/docs/reference//351/201/213/347/224/250/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +392 -392
- package/lib/assets/docs/reference//351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +299 -299
- package/lib/assets/docs/reference//351/235/236/346/251/237/350/203/275/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1236 -1236
- package/lib/assets/docs/review/index.md +5 -5
- package/lib/assets/docs/strategy/index.md +1 -1
- package/lib/assets/docs/template/ADR.md +30 -30
- package/lib/assets/docs/template/AWS/343/202/271/343/203/206/343/203/274/343/202/270/343/203/263/343/202/260/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +1366 -1366
- package/lib/assets/docs/template/AWS/343/203/227/343/203/255/343/203/200/343/202/257/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +634 -634
- package/lib/assets/docs/template/README.md +50 -50
- package/lib/assets/docs/template/index.md +23 -23
- package/lib/assets/docs/template//343/201/276/343/201/232/343/201/223/343/202/214/343/202/222/350/252/255/343/202/202/343/201/206/343/203/252/343/202/271/343/203/210.md +12 -12
- package/lib/assets/docs/template//343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +547 -547
- package/lib/assets/docs/template//343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/345/256/214/344/272/206/345/240/261/345/221/212/346/233/270.md +58 -58
- package/lib/assets/docs/template//343/202/244/343/203/263/343/202/273/343/203/227/343/202/267/343/203/247/343/203/263/343/203/207/343/203/203/343/202/255.md +13 -13
- package/lib/assets/docs/template//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243.md +379 -379
- package/lib/assets/docs/template//344/274/201/346/245/255/345/210/206/346/236/220.md +573 -573
- package/lib/assets/docs/template//345/256/214/345/205/250/345/275/242/345/274/217/343/201/256/343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271.md +69 -68
- package/lib/assets/docs/template//350/246/201/344/273/266/345/256/232/347/276/251.md +669 -669
- package/lib/assets/docs/template//350/250/255/350/250/210.md +173 -173
- package/lib/assets/docs/template//351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +688 -688
- package/lib/assets/gulpfile.js +25 -25
- package/lib/assets/mkdocs.yml +136 -135
- package/lib/assets/ops/docker/mkdoc/Dockerfile +19 -19
- package/lib/assets/ops/scripts/journal.js +180 -180
- package/lib/assets/ops/scripts/mkdocs.js +82 -82
- package/lib/assets/ops/scripts/release.js +431 -431
- package/lib/assets/ops/scripts/sonar_local.js +726 -726
- package/lib/assets/ops/scripts/ssh.js +190 -190
- package/lib/assets/ops/scripts/vault.js +299 -299
- package/lib/assets/package-lock.json +1653 -1653
- package/lib/assets/package.json +40 -40
- package/lib/gulpfile.js +37 -37
- package/package.json +41 -41
- package/lib/assets/.claude/agent-memory/xp-programmer/MEMORY.md +0 -6
- package/lib/assets/.claude/agent-memory/xp-programmer/project_cargo_tracker.md +0 -11
- package/lib/assets/.claude/agent-memory/xp-programmer/project_ddd_patterns.md +0 -27
- package/lib/assets/.claude/agent-memory/xp-programmer/project_us07_route_assignment.md +0 -19
|
@@ -1,1698 +1,1698 @@
|
|
|
1
|
-
# 第38章:API 設計とサービス連携
|
|
2
|
-
|
|
3
|
-
本章では、基幹業務システムにおける API 設計の原則と、サービス間連携のパターンについて解説します。RESTful API の設計、サービス間通信の方式、API ゲートウェイの活用、そしてインテグレーションテストの実践方法を学びます。
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 38.1 API 設計の原則
|
|
8
|
-
|
|
9
|
-
### RESTful API の設計
|
|
10
|
-
|
|
11
|
-
REST(Representational State Transfer)は、Web API 設計の標準的なアーキテクチャスタイルです。
|
|
12
|
-
|
|
13
|
-
```plantuml
|
|
14
|
-
@startuml
|
|
15
|
-
title RESTful API の基本原則
|
|
16
|
-
|
|
17
|
-
rectangle "REST の制約" as constraints {
|
|
18
|
-
rectangle "クライアント-サーバー" as cs {
|
|
19
|
-
note right
|
|
20
|
-
関心の分離
|
|
21
|
-
独立した進化
|
|
22
|
-
end note
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
rectangle "ステートレス" as stateless {
|
|
26
|
-
note right
|
|
27
|
-
各リクエストは独立
|
|
28
|
-
セッション状態を保持しない
|
|
29
|
-
end note
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
rectangle "キャッシュ可能" as cacheable {
|
|
33
|
-
note right
|
|
34
|
-
レスポンスにキャッシュ可否を明示
|
|
35
|
-
パフォーマンス向上
|
|
36
|
-
end note
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
rectangle "統一インターフェース" as uniform {
|
|
40
|
-
note right
|
|
41
|
-
リソース識別(URI)
|
|
42
|
-
表現によるリソース操作
|
|
43
|
-
自己記述メッセージ
|
|
44
|
-
HATEOAS
|
|
45
|
-
end note
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
rectangle "階層化システム" as layered {
|
|
49
|
-
note right
|
|
50
|
-
中間サーバーの追加が可能
|
|
51
|
-
ロードバランサー、キャッシュ等
|
|
52
|
-
end note
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@enduml
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
#### HTTP メソッドとリソース操作
|
|
60
|
-
|
|
61
|
-
| HTTP メソッド | 操作 | 冪等性 | 安全性 | 使用例 |
|
|
62
|
-
|-------------|-----|-------|-------|-------|
|
|
63
|
-
| GET | 取得 | Yes | Yes | リソースの参照 |
|
|
64
|
-
| POST | 作成 | No | No | 新規リソース作成 |
|
|
65
|
-
| PUT | 置換 | Yes | No | リソース全体の更新 |
|
|
66
|
-
| PATCH | 部分更新 | No | No | リソースの一部更新 |
|
|
67
|
-
| DELETE | 削除 | Yes | No | リソースの削除 |
|
|
68
|
-
|
|
69
|
-
### リソース指向設計
|
|
70
|
-
|
|
71
|
-
API はリソース(名詞)を中心に設計し、操作は HTTP メソッドで表現します。
|
|
72
|
-
|
|
73
|
-
```plantuml
|
|
74
|
-
@startuml
|
|
75
|
-
title リソース指向 API 設計
|
|
76
|
-
|
|
77
|
-
package "販売管理 API" as sales_api {
|
|
78
|
-
rectangle "/orders" as orders {
|
|
79
|
-
rectangle "GET /orders" as get_orders
|
|
80
|
-
rectangle "POST /orders" as post_order
|
|
81
|
-
rectangle "GET /orders/{id}" as get_order
|
|
82
|
-
rectangle "PUT /orders/{id}" as put_order
|
|
83
|
-
rectangle "DELETE /orders/{id}" as delete_order
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
rectangle "/orders/{id}/lines" as order_lines {
|
|
87
|
-
rectangle "GET /orders/{id}/lines" as get_lines
|
|
88
|
-
rectangle "POST /orders/{id}/lines" as post_line
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
rectangle "/customers" as customers {
|
|
92
|
-
rectangle "GET /customers" as get_customers
|
|
93
|
-
rectangle "POST /customers" as post_customer
|
|
94
|
-
rectangle "GET /customers/{id}" as get_customer
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
rectangle "/customers/{id}/orders" as customer_orders {
|
|
98
|
-
rectangle "GET /customers/{id}/orders" as get_customer_orders
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
note bottom of sales_api
|
|
103
|
-
【設計原則】
|
|
104
|
-
・リソースは名詞(複数形)
|
|
105
|
-
・操作はHTTPメソッドで表現
|
|
106
|
-
・階層構造で関連を表現
|
|
107
|
-
・クエリパラメータでフィルタリング
|
|
108
|
-
end note
|
|
109
|
-
|
|
110
|
-
@enduml
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
#### 基幹業務システムの API エンドポイント設計
|
|
114
|
-
|
|
115
|
-
```plantuml
|
|
116
|
-
@startuml
|
|
117
|
-
title 基幹業務システム API エンドポイント
|
|
118
|
-
|
|
119
|
-
package "販売管理 API" as sales {
|
|
120
|
-
rectangle "/api/v1/sales" as sales_base {
|
|
121
|
-
rectangle "受注: /orders"
|
|
122
|
-
rectangle "出荷: /shipments"
|
|
123
|
-
rectangle "売上: /sales"
|
|
124
|
-
rectangle "請求: /invoices"
|
|
125
|
-
rectangle "入金: /payments"
|
|
126
|
-
}
|
|
127
|
-
rectangle "/api/v1/sales/masters" as sales_masters {
|
|
128
|
-
rectangle "顧客: /customers"
|
|
129
|
-
rectangle "商品: /products"
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
package "財務会計 API" as accounting {
|
|
134
|
-
rectangle "/api/v1/accounting" as acc_base {
|
|
135
|
-
rectangle "仕訳: /journals"
|
|
136
|
-
rectangle "残高: /balances"
|
|
137
|
-
rectangle "試算表: /trial-balances"
|
|
138
|
-
}
|
|
139
|
-
rectangle "/api/v1/accounting/masters" as acc_masters {
|
|
140
|
-
rectangle "勘定科目: /accounts"
|
|
141
|
-
rectangle "補助科目: /sub-accounts"
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
package "生産管理 API" as production {
|
|
146
|
-
rectangle "/api/v1/production" as prod_base {
|
|
147
|
-
rectangle "製造指示: /work-orders"
|
|
148
|
-
rectangle "発注: /purchase-orders"
|
|
149
|
-
rectangle "在庫: /inventory"
|
|
150
|
-
}
|
|
151
|
-
rectangle "/api/v1/production/masters" as prod_masters {
|
|
152
|
-
rectangle "品目: /items"
|
|
153
|
-
rectangle "BOM: /bom"
|
|
154
|
-
rectangle "工程: /processes"
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@enduml
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
<details>
|
|
162
|
-
<summary>OpenAPI 定義例</summary>
|
|
163
|
-
|
|
164
|
-
```yaml
|
|
165
|
-
openapi: 3.0.3
|
|
166
|
-
info:
|
|
167
|
-
title: 販売管理 API
|
|
168
|
-
version: 1.0.0
|
|
169
|
-
description: 基幹業務システム - 販売管理 API
|
|
170
|
-
|
|
171
|
-
servers:
|
|
172
|
-
- url: https://api.example.com/api/v1/sales
|
|
173
|
-
description: Production server
|
|
174
|
-
|
|
175
|
-
paths:
|
|
176
|
-
/orders:
|
|
177
|
-
get:
|
|
178
|
-
summary: 受注一覧取得
|
|
179
|
-
operationId: getOrders
|
|
180
|
-
parameters:
|
|
181
|
-
- name: customerId
|
|
182
|
-
in: query
|
|
183
|
-
schema:
|
|
184
|
-
type: string
|
|
185
|
-
- name: status
|
|
186
|
-
in: query
|
|
187
|
-
schema:
|
|
188
|
-
type: string
|
|
189
|
-
enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
|
|
190
|
-
- name: fromDate
|
|
191
|
-
in: query
|
|
192
|
-
schema:
|
|
193
|
-
type: string
|
|
194
|
-
format: date
|
|
195
|
-
- name: toDate
|
|
196
|
-
in: query
|
|
197
|
-
schema:
|
|
198
|
-
type: string
|
|
199
|
-
format: date
|
|
200
|
-
- name: page
|
|
201
|
-
in: query
|
|
202
|
-
schema:
|
|
203
|
-
type: integer
|
|
204
|
-
default: 0
|
|
205
|
-
- name: size
|
|
206
|
-
in: query
|
|
207
|
-
schema:
|
|
208
|
-
type: integer
|
|
209
|
-
default: 20
|
|
210
|
-
responses:
|
|
211
|
-
'200':
|
|
212
|
-
description: 成功
|
|
213
|
-
content:
|
|
214
|
-
application/json:
|
|
215
|
-
schema:
|
|
216
|
-
$ref: '#/components/schemas/OrderListResponse'
|
|
217
|
-
|
|
218
|
-
post:
|
|
219
|
-
summary: 受注登録
|
|
220
|
-
operationId: createOrder
|
|
221
|
-
requestBody:
|
|
222
|
-
required: true
|
|
223
|
-
content:
|
|
224
|
-
application/json:
|
|
225
|
-
schema:
|
|
226
|
-
$ref: '#/components/schemas/CreateOrderRequest'
|
|
227
|
-
responses:
|
|
228
|
-
'201':
|
|
229
|
-
description: 作成成功
|
|
230
|
-
content:
|
|
231
|
-
application/json:
|
|
232
|
-
schema:
|
|
233
|
-
$ref: '#/components/schemas/OrderResponse'
|
|
234
|
-
'400':
|
|
235
|
-
description: バリデーションエラー
|
|
236
|
-
content:
|
|
237
|
-
application/problem+json:
|
|
238
|
-
schema:
|
|
239
|
-
$ref: '#/components/schemas/ProblemDetail'
|
|
240
|
-
|
|
241
|
-
/orders/{orderId}:
|
|
242
|
-
get:
|
|
243
|
-
summary: 受注詳細取得
|
|
244
|
-
operationId: getOrder
|
|
245
|
-
parameters:
|
|
246
|
-
- name: orderId
|
|
247
|
-
in: path
|
|
248
|
-
required: true
|
|
249
|
-
schema:
|
|
250
|
-
type: string
|
|
251
|
-
responses:
|
|
252
|
-
'200':
|
|
253
|
-
description: 成功
|
|
254
|
-
content:
|
|
255
|
-
application/json:
|
|
256
|
-
schema:
|
|
257
|
-
$ref: '#/components/schemas/OrderResponse'
|
|
258
|
-
'404':
|
|
259
|
-
description: 受注が見つからない
|
|
260
|
-
|
|
261
|
-
components:
|
|
262
|
-
schemas:
|
|
263
|
-
OrderResponse:
|
|
264
|
-
type: object
|
|
265
|
-
properties:
|
|
266
|
-
orderId:
|
|
267
|
-
type: string
|
|
268
|
-
customerId:
|
|
269
|
-
type: string
|
|
270
|
-
customerName:
|
|
271
|
-
type: string
|
|
272
|
-
orderDate:
|
|
273
|
-
type: string
|
|
274
|
-
format: date
|
|
275
|
-
status:
|
|
276
|
-
type: string
|
|
277
|
-
enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
|
|
278
|
-
totalAmount:
|
|
279
|
-
type: number
|
|
280
|
-
taxAmount:
|
|
281
|
-
type: number
|
|
282
|
-
lines:
|
|
283
|
-
type: array
|
|
284
|
-
items:
|
|
285
|
-
$ref: '#/components/schemas/OrderLineResponse'
|
|
286
|
-
_links:
|
|
287
|
-
$ref: '#/components/schemas/Links'
|
|
288
|
-
|
|
289
|
-
CreateOrderRequest:
|
|
290
|
-
type: object
|
|
291
|
-
required:
|
|
292
|
-
- customerId
|
|
293
|
-
- orderDate
|
|
294
|
-
- lines
|
|
295
|
-
properties:
|
|
296
|
-
customerId:
|
|
297
|
-
type: string
|
|
298
|
-
orderDate:
|
|
299
|
-
type: string
|
|
300
|
-
format: date
|
|
301
|
-
requestedDeliveryDate:
|
|
302
|
-
type: string
|
|
303
|
-
format: date
|
|
304
|
-
lines:
|
|
305
|
-
type: array
|
|
306
|
-
items:
|
|
307
|
-
$ref: '#/components/schemas/CreateOrderLineRequest'
|
|
308
|
-
minItems: 1
|
|
309
|
-
|
|
310
|
-
ProblemDetail:
|
|
311
|
-
type: object
|
|
312
|
-
properties:
|
|
313
|
-
type:
|
|
314
|
-
type: string
|
|
315
|
-
title:
|
|
316
|
-
type: string
|
|
317
|
-
status:
|
|
318
|
-
type: integer
|
|
319
|
-
detail:
|
|
320
|
-
type: string
|
|
321
|
-
instance:
|
|
322
|
-
type: string
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
</details>
|
|
326
|
-
|
|
327
|
-
### バージョニング戦略
|
|
328
|
-
|
|
329
|
-
API の互換性を維持しながら進化させるためのバージョニング戦略を検討します。
|
|
330
|
-
|
|
331
|
-
```plantuml
|
|
332
|
-
@startuml
|
|
333
|
-
title API バージョニング戦略の比較
|
|
334
|
-
|
|
335
|
-
rectangle "URI パスバージョニング" as uri {
|
|
336
|
-
note right
|
|
337
|
-
/api/v1/orders
|
|
338
|
-
/api/v2/orders
|
|
339
|
-
|
|
340
|
-
【メリット】
|
|
341
|
-
・明確で分かりやすい
|
|
342
|
-
・キャッシュしやすい
|
|
343
|
-
|
|
344
|
-
【デメリット】
|
|
345
|
-
・URIの変更が必要
|
|
346
|
-
end note
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
rectangle "クエリパラメータ" as query {
|
|
350
|
-
note right
|
|
351
|
-
/api/orders?version=1
|
|
352
|
-
/api/orders?version=2
|
|
353
|
-
|
|
354
|
-
【メリット】
|
|
355
|
-
・URIが変わらない
|
|
356
|
-
|
|
357
|
-
【デメリット】
|
|
358
|
-
・見落としやすい
|
|
359
|
-
end note
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
rectangle "カスタムヘッダー" as header {
|
|
363
|
-
note right
|
|
364
|
-
X-API-Version: 1
|
|
365
|
-
X-API-Version: 2
|
|
366
|
-
|
|
367
|
-
【メリット】
|
|
368
|
-
・URIがクリーン
|
|
369
|
-
|
|
370
|
-
【デメリット】
|
|
371
|
-
・テストしにくい
|
|
372
|
-
end note
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
rectangle "Accept ヘッダー" as accept {
|
|
376
|
-
note right
|
|
377
|
-
Accept: application/vnd.company.v1+json
|
|
378
|
-
Accept: application/vnd.company.v2+json
|
|
379
|
-
|
|
380
|
-
【メリット】
|
|
381
|
-
・HTTP標準に準拠
|
|
382
|
-
|
|
383
|
-
【デメリット】
|
|
384
|
-
・複雑
|
|
385
|
-
end note
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
@enduml
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
#### 推奨:URI パスバージョニング
|
|
392
|
-
|
|
393
|
-
```plantuml
|
|
394
|
-
@startuml
|
|
395
|
-
title URI パスバージョニングの運用
|
|
396
|
-
|
|
397
|
-
rectangle "v1(現行)" as v1 {
|
|
398
|
-
rectangle "/api/v1/orders" as v1_orders
|
|
399
|
-
rectangle "/api/v1/customers" as v1_customers
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
rectangle "v2(新版)" as v2 {
|
|
403
|
-
rectangle "/api/v2/orders" as v2_orders
|
|
404
|
-
rectangle "/api/v2/customers" as v2_customers
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
note bottom of v1
|
|
408
|
-
【v1 → v2 移行】
|
|
409
|
-
1. v2を新規リリース
|
|
410
|
-
2. 並行運用期間
|
|
411
|
-
3. v1を非推奨化(Deprecation)
|
|
412
|
-
4. v1を廃止(Sunset)
|
|
413
|
-
|
|
414
|
-
【Deprecation ヘッダー】
|
|
415
|
-
Deprecation: true
|
|
416
|
-
Sunset: Sat, 01 Jan 2025 00:00:00 GMT
|
|
417
|
-
end note
|
|
418
|
-
|
|
419
|
-
@enduml
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
---
|
|
423
|
-
|
|
424
|
-
## 38.2 サービス間通信
|
|
425
|
-
|
|
426
|
-
### 同期通信(REST / gRPC)
|
|
427
|
-
|
|
428
|
-
```plantuml
|
|
429
|
-
@startuml
|
|
430
|
-
title 同期通信パターンの比較
|
|
431
|
-
|
|
432
|
-
rectangle "REST over HTTP" as rest {
|
|
433
|
-
rectangle "販売サービス" as sales_rest
|
|
434
|
-
rectangle "在庫サービス" as inv_rest
|
|
435
|
-
|
|
436
|
-
sales_rest --> inv_rest : HTTP/JSON
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
rectangle "gRPC" as grpc {
|
|
440
|
-
rectangle "販売サービス" as sales_grpc
|
|
441
|
-
rectangle "在庫サービス" as inv_grpc
|
|
442
|
-
|
|
443
|
-
sales_grpc --> inv_grpc : HTTP/2 + Protocol Buffers
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
note bottom of rest
|
|
447
|
-
【REST】
|
|
448
|
-
・テキストベース(JSON)
|
|
449
|
-
・ブラウザから直接呼び出し可能
|
|
450
|
-
・広く普及、ツール豊富
|
|
451
|
-
|
|
452
|
-
【適用場面】
|
|
453
|
-
・外部公開API
|
|
454
|
-
・フロントエンド連携
|
|
455
|
-
end note
|
|
456
|
-
|
|
457
|
-
note bottom of grpc
|
|
458
|
-
【gRPC】
|
|
459
|
-
・バイナリプロトコル
|
|
460
|
-
・高速、低レイテンシ
|
|
461
|
-
・スキーマ定義必須
|
|
462
|
-
|
|
463
|
-
【適用場面】
|
|
464
|
-
・マイクロサービス間
|
|
465
|
-
・高性能要件
|
|
466
|
-
end note
|
|
467
|
-
|
|
468
|
-
@enduml
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
<details>
|
|
472
|
-
<summary>gRPC 定義例</summary>
|
|
473
|
-
|
|
474
|
-
```protobuf
|
|
475
|
-
syntax = "proto3";
|
|
476
|
-
|
|
477
|
-
package sales.v1;
|
|
478
|
-
|
|
479
|
-
option java_package = "com.example.sales.grpc";
|
|
480
|
-
option java_multiple_files = true;
|
|
481
|
-
|
|
482
|
-
// 在庫サービス
|
|
483
|
-
service InventoryService {
|
|
484
|
-
// 在庫照会
|
|
485
|
-
rpc GetStock(GetStockRequest) returns (StockResponse);
|
|
486
|
-
|
|
487
|
-
// 在庫引当
|
|
488
|
-
rpc AllocateStock(AllocateStockRequest) returns (AllocateStockResponse);
|
|
489
|
-
|
|
490
|
-
// 引当解除
|
|
491
|
-
rpc ReleaseStock(ReleaseStockRequest) returns (ReleaseStockResponse);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
message GetStockRequest {
|
|
495
|
-
string product_id = 1;
|
|
496
|
-
string warehouse_id = 2;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
message StockResponse {
|
|
500
|
-
string product_id = 1;
|
|
501
|
-
string warehouse_id = 2;
|
|
502
|
-
int32 quantity = 3;
|
|
503
|
-
int32 allocated_quantity = 4;
|
|
504
|
-
int32 available_quantity = 5;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
message AllocateStockRequest {
|
|
508
|
-
string product_id = 1;
|
|
509
|
-
string warehouse_id = 2;
|
|
510
|
-
int32 quantity = 3;
|
|
511
|
-
string order_id = 4;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
message AllocateStockResponse {
|
|
515
|
-
bool success = 1;
|
|
516
|
-
string allocation_id = 2;
|
|
517
|
-
string error_message = 3;
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
</details>
|
|
522
|
-
|
|
523
|
-
### 非同期通信(メッセージキュー)
|
|
524
|
-
|
|
525
|
-
```plantuml
|
|
526
|
-
@startuml
|
|
527
|
-
title 非同期通信パターン
|
|
528
|
-
|
|
529
|
-
rectangle "販売サービス" as sales
|
|
530
|
-
|
|
531
|
-
rectangle "メッセージブローカー" as broker {
|
|
532
|
-
queue "受注キュー" as order_queue
|
|
533
|
-
collections "売上トピック" as sales_topic
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
rectangle "在庫サービス" as inventory
|
|
537
|
-
rectangle "会計サービス" as accounting
|
|
538
|
-
rectangle "通知サービス" as notification
|
|
539
|
-
|
|
540
|
-
sales --> order_queue : 受注メッセージ
|
|
541
|
-
order_queue --> inventory : 在庫引当
|
|
542
|
-
|
|
543
|
-
sales --> sales_topic : 売上イベント
|
|
544
|
-
sales_topic --> accounting : 仕訳生成
|
|
545
|
-
sales_topic --> notification : 通知送信
|
|
546
|
-
sales_topic --> inventory : 在庫更新
|
|
547
|
-
|
|
548
|
-
note bottom of broker
|
|
549
|
-
【Point-to-Point】
|
|
550
|
-
・1対1の通信
|
|
551
|
-
・キューで実現
|
|
552
|
-
・負荷分散可能
|
|
553
|
-
|
|
554
|
-
【Publish-Subscribe】
|
|
555
|
-
・1対多の通信
|
|
556
|
-
・トピックで実現
|
|
557
|
-
・イベント配信
|
|
558
|
-
end note
|
|
559
|
-
|
|
560
|
-
@enduml
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
<details>
|
|
564
|
-
<summary>Java 実装例(Spring AMQP)</summary>
|
|
565
|
-
|
|
566
|
-
```java
|
|
567
|
-
// メッセージ送信
|
|
568
|
-
@Service
|
|
569
|
-
public class OrderMessagePublisher {
|
|
570
|
-
private final RabbitTemplate rabbitTemplate;
|
|
571
|
-
|
|
572
|
-
public void publishOrderCreated(OrderCreatedEvent event) {
|
|
573
|
-
rabbitTemplate.convertAndSend(
|
|
574
|
-
"sales.exchange",
|
|
575
|
-
"order.created",
|
|
576
|
-
event
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
public void publishSalesCompleted(SalesCompletedEvent event) {
|
|
581
|
-
rabbitTemplate.convertAndSend(
|
|
582
|
-
"sales.topic.exchange",
|
|
583
|
-
"sales.completed",
|
|
584
|
-
event
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// メッセージ受信
|
|
590
|
-
@Component
|
|
591
|
-
public class OrderMessageListener {
|
|
592
|
-
|
|
593
|
-
@RabbitListener(queues = "inventory.order.queue")
|
|
594
|
-
public void handleOrderCreated(OrderCreatedEvent event) {
|
|
595
|
-
// 在庫引当処理
|
|
596
|
-
log.info("Received order: {}", event.orderId());
|
|
597
|
-
inventoryService.allocate(event);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// 設定
|
|
602
|
-
@Configuration
|
|
603
|
-
public class RabbitMQConfig {
|
|
604
|
-
|
|
605
|
-
@Bean
|
|
606
|
-
public TopicExchange salesTopicExchange() {
|
|
607
|
-
return new TopicExchange("sales.topic.exchange");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
@Bean
|
|
611
|
-
public Queue accountingQueue() {
|
|
612
|
-
return new Queue("accounting.sales.queue", true);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
@Bean
|
|
616
|
-
public Binding accountingBinding(Queue accountingQueue,
|
|
617
|
-
TopicExchange salesTopicExchange) {
|
|
618
|
-
return BindingBuilder.bind(accountingQueue)
|
|
619
|
-
.to(salesTopicExchange)
|
|
620
|
-
.with("sales.*");
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
</details>
|
|
626
|
-
|
|
627
|
-
### サーキットブレーカーパターン
|
|
628
|
-
|
|
629
|
-
サービス間通信の障害を検知し、障害の連鎖を防ぐパターンです。
|
|
630
|
-
|
|
631
|
-
```plantuml
|
|
632
|
-
@startuml
|
|
633
|
-
title サーキットブレーカーの状態遷移
|
|
634
|
-
|
|
635
|
-
[*] --> Closed : 初期状態
|
|
636
|
-
|
|
637
|
-
Closed --> Open : 失敗が閾値を超過
|
|
638
|
-
Closed --> Closed : 成功
|
|
639
|
-
|
|
640
|
-
Open --> HalfOpen : タイムアウト後
|
|
641
|
-
Open --> Open : リクエスト拒否
|
|
642
|
-
|
|
643
|
-
HalfOpen --> Closed : 試行成功
|
|
644
|
-
HalfOpen --> Open : 試行失敗
|
|
645
|
-
|
|
646
|
-
note right of Closed
|
|
647
|
-
【Closed】
|
|
648
|
-
通常稼働
|
|
649
|
-
リクエストを通す
|
|
650
|
-
end note
|
|
651
|
-
|
|
652
|
-
note right of Open
|
|
653
|
-
【Open】
|
|
654
|
-
障害検知
|
|
655
|
-
即座にエラー返却
|
|
656
|
-
(Fail Fast)
|
|
657
|
-
end note
|
|
658
|
-
|
|
659
|
-
note right of HalfOpen
|
|
660
|
-
【Half-Open】
|
|
661
|
-
復旧確認中
|
|
662
|
-
限定的に試行
|
|
663
|
-
end note
|
|
664
|
-
|
|
665
|
-
@enduml
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
```plantuml
|
|
669
|
-
@startuml
|
|
670
|
-
title サーキットブレーカーの動作フロー
|
|
671
|
-
|
|
672
|
-
|クライアント|
|
|
673
|
-
start
|
|
674
|
-
:サービス呼び出し;
|
|
675
|
-
|
|
676
|
-
|サーキットブレーカー|
|
|
677
|
-
if (回路状態?) then (Open)
|
|
678
|
-
:即座にフォールバック;
|
|
679
|
-
|クライアント|
|
|
680
|
-
:フォールバック結果;
|
|
681
|
-
stop
|
|
682
|
-
else (Closed/HalfOpen)
|
|
683
|
-
endif
|
|
684
|
-
|
|
685
|
-
:下流サービス呼び出し;
|
|
686
|
-
|
|
687
|
-
|下流サービス|
|
|
688
|
-
if (処理成功?) then (yes)
|
|
689
|
-
:正常レスポンス;
|
|
690
|
-
|サーキットブレーカー|
|
|
691
|
-
:成功をカウント;
|
|
692
|
-
:Closedを維持/遷移;
|
|
693
|
-
else (no)
|
|
694
|
-
:エラー/タイムアウト;
|
|
695
|
-
|サーキットブレーカー|
|
|
696
|
-
:失敗をカウント;
|
|
697
|
-
|
|
698
|
-
if (失敗数 >= 閾値?) then (yes)
|
|
699
|
-
:Openに遷移;
|
|
700
|
-
endif
|
|
701
|
-
endif
|
|
702
|
-
|
|
703
|
-
|クライアント|
|
|
704
|
-
:結果を返却;
|
|
705
|
-
|
|
706
|
-
stop
|
|
707
|
-
|
|
708
|
-
@enduml
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
<details>
|
|
712
|
-
<summary>Java 実装例(Resilience4j)</summary>
|
|
713
|
-
|
|
714
|
-
```java
|
|
715
|
-
// 設定
|
|
716
|
-
@Configuration
|
|
717
|
-
public class CircuitBreakerConfig {
|
|
718
|
-
|
|
719
|
-
@Bean
|
|
720
|
-
public CircuitBreakerRegistry circuitBreakerRegistry() {
|
|
721
|
-
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
|
|
722
|
-
.failureRateThreshold(50) // 失敗率50%でOpen
|
|
723
|
-
.waitDurationInOpenState(Duration.ofSeconds(30))
|
|
724
|
-
.permittedNumberOfCallsInHalfOpenState(3)
|
|
725
|
-
.slidingWindowType(SlidingWindowType.COUNT_BASED)
|
|
726
|
-
.slidingWindowSize(10)
|
|
727
|
-
.build();
|
|
728
|
-
|
|
729
|
-
return CircuitBreakerRegistry.of(config);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// サービス
|
|
734
|
-
@Service
|
|
735
|
-
public class InventoryServiceClient {
|
|
736
|
-
private final RestTemplate restTemplate;
|
|
737
|
-
private final CircuitBreaker circuitBreaker;
|
|
738
|
-
|
|
739
|
-
public InventoryServiceClient(RestTemplate restTemplate,
|
|
740
|
-
CircuitBreakerRegistry registry) {
|
|
741
|
-
this.restTemplate = restTemplate;
|
|
742
|
-
this.circuitBreaker = registry.circuitBreaker("inventoryService");
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
public StockResponse getStock(String productId) {
|
|
746
|
-
Supplier<StockResponse> supplier = CircuitBreaker
|
|
747
|
-
.decorateSupplier(circuitBreaker, () -> {
|
|
748
|
-
return restTemplate.getForObject(
|
|
749
|
-
"/api/v1/inventory/{productId}",
|
|
750
|
-
StockResponse.class,
|
|
751
|
-
productId
|
|
752
|
-
);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
return Try.ofSupplier(supplier)
|
|
756
|
-
.recover(CallNotPermittedException.class,
|
|
757
|
-
e -> getFallbackStock(productId))
|
|
758
|
-
.recover(Exception.class,
|
|
759
|
-
e -> getFallbackStock(productId))
|
|
760
|
-
.get();
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private StockResponse getFallbackStock(String productId) {
|
|
764
|
-
// フォールバック: キャッシュから取得または推定値
|
|
765
|
-
return new StockResponse(productId, 0, 0, 0, "UNKNOWN");
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// アノテーションベース
|
|
770
|
-
@Service
|
|
771
|
-
public class AccountingServiceClient {
|
|
772
|
-
|
|
773
|
-
@CircuitBreaker(name = "accountingService", fallbackMethod = "fallback")
|
|
774
|
-
@Retry(name = "accountingService")
|
|
775
|
-
@TimeLimiter(name = "accountingService")
|
|
776
|
-
public CompletableFuture<JournalResponse> createJournal(
|
|
777
|
-
CreateJournalRequest request) {
|
|
778
|
-
return CompletableFuture.supplyAsync(() -> {
|
|
779
|
-
return restTemplate.postForObject(
|
|
780
|
-
"/api/v1/accounting/journals",
|
|
781
|
-
request,
|
|
782
|
-
JournalResponse.class
|
|
783
|
-
);
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
public CompletableFuture<JournalResponse> fallback(
|
|
788
|
-
CreateJournalRequest request, Throwable t) {
|
|
789
|
-
log.warn("Fallback for createJournal: {}", t.getMessage());
|
|
790
|
-
// 仕訳をキューに保存して後で再試行
|
|
791
|
-
pendingJournalQueue.add(request);
|
|
792
|
-
return CompletableFuture.completedFuture(
|
|
793
|
-
JournalResponse.pending(request.getCorrelationId())
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
</details>
|
|
800
|
-
|
|
801
|
-
---
|
|
802
|
-
|
|
803
|
-
## 38.3 API ゲートウェイ
|
|
804
|
-
|
|
805
|
-
API ゲートウェイは、クライアントと複数のバックエンドサービスの間に位置し、横断的な関心事を一元的に処理します。
|
|
806
|
-
|
|
807
|
-
```plantuml
|
|
808
|
-
@startuml
|
|
809
|
-
title API ゲートウェイアーキテクチャ
|
|
810
|
-
|
|
811
|
-
rectangle "クライアント" as clients {
|
|
812
|
-
rectangle "Webアプリ" as web
|
|
813
|
-
rectangle "モバイルアプリ" as mobile
|
|
814
|
-
rectangle "外部システム" as external
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
rectangle "API Gateway" as gateway {
|
|
818
|
-
rectangle "認証・認可" as auth
|
|
819
|
-
rectangle "レート制限" as rate
|
|
820
|
-
rectangle "ルーティング" as routing
|
|
821
|
-
rectangle "ログ・監視" as logging
|
|
822
|
-
rectangle "変換・集約" as transform
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
rectangle "バックエンドサービス" as backend {
|
|
826
|
-
rectangle "販売API" as sales
|
|
827
|
-
rectangle "会計API" as accounting
|
|
828
|
-
rectangle "生産API" as production
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
web --> gateway
|
|
832
|
-
mobile --> gateway
|
|
833
|
-
external --> gateway
|
|
834
|
-
|
|
835
|
-
gateway --> sales
|
|
836
|
-
gateway --> accounting
|
|
837
|
-
gateway --> production
|
|
838
|
-
|
|
839
|
-
note bottom of gateway
|
|
840
|
-
【API Gatewayの責務】
|
|
841
|
-
・認証・認可の一元化
|
|
842
|
-
・レート制限
|
|
843
|
-
・リクエストルーティング
|
|
844
|
-
・ログ集約
|
|
845
|
-
・レスポンス変換
|
|
846
|
-
・API集約
|
|
847
|
-
end note
|
|
848
|
-
|
|
849
|
-
@enduml
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
### 認証・認可の一元化
|
|
853
|
-
|
|
854
|
-
```plantuml
|
|
855
|
-
@startuml
|
|
856
|
-
title JWT 認証フロー
|
|
857
|
-
|
|
858
|
-
|クライアント|
|
|
859
|
-
start
|
|
860
|
-
:認証リクエスト;
|
|
861
|
-
note right
|
|
862
|
-
POST /auth/login
|
|
863
|
-
{username, password}
|
|
864
|
-
end note
|
|
865
|
-
|
|
866
|
-
|認証サービス|
|
|
867
|
-
:認証情報検証;
|
|
868
|
-
:JWTトークン発行;
|
|
869
|
-
note right
|
|
870
|
-
Header: {alg, typ}
|
|
871
|
-
Payload: {sub, roles, exp}
|
|
872
|
-
Signature
|
|
873
|
-
end note
|
|
874
|
-
|
|
875
|
-
|クライアント|
|
|
876
|
-
:JWTトークン保存;
|
|
877
|
-
:API呼び出し;
|
|
878
|
-
note right
|
|
879
|
-
Authorization: Bearer {token}
|
|
880
|
-
end note
|
|
881
|
-
|
|
882
|
-
|API Gateway|
|
|
883
|
-
:JWTトークン検証;
|
|
884
|
-
if (有効?) then (yes)
|
|
885
|
-
:クレーム抽出;
|
|
886
|
-
:認可チェック;
|
|
887
|
-
if (権限あり?) then (yes)
|
|
888
|
-
|バックエンドサービス|
|
|
889
|
-
:リクエスト処理;
|
|
890
|
-
:レスポンス返却;
|
|
891
|
-
else (no)
|
|
892
|
-
:403 Forbidden;
|
|
893
|
-
endif
|
|
894
|
-
else (no)
|
|
895
|
-
:401 Unauthorized;
|
|
896
|
-
endif
|
|
897
|
-
|
|
898
|
-
|クライアント|
|
|
899
|
-
:結果受信;
|
|
900
|
-
|
|
901
|
-
stop
|
|
902
|
-
|
|
903
|
-
@enduml
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
<details>
|
|
907
|
-
<summary>Java 実装例(Spring Security + JWT)</summary>
|
|
908
|
-
|
|
909
|
-
```java
|
|
910
|
-
// JWT フィルター
|
|
911
|
-
@Component
|
|
912
|
-
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
913
|
-
private final JwtTokenProvider tokenProvider;
|
|
914
|
-
|
|
915
|
-
@Override
|
|
916
|
-
protected void doFilterInternal(HttpServletRequest request,
|
|
917
|
-
HttpServletResponse response,
|
|
918
|
-
FilterChain filterChain)
|
|
919
|
-
throws ServletException, IOException {
|
|
920
|
-
|
|
921
|
-
String token = extractToken(request);
|
|
922
|
-
|
|
923
|
-
if (token != null && tokenProvider.validateToken(token)) {
|
|
924
|
-
Authentication auth = tokenProvider.getAuthentication(token);
|
|
925
|
-
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
filterChain.doFilter(request, response);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
private String extractToken(HttpServletRequest request) {
|
|
932
|
-
String bearerToken = request.getHeader("Authorization");
|
|
933
|
-
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
|
934
|
-
return bearerToken.substring(7);
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// JWT プロバイダー
|
|
941
|
-
@Component
|
|
942
|
-
public class JwtTokenProvider {
|
|
943
|
-
@Value("${jwt.secret}")
|
|
944
|
-
private String jwtSecret;
|
|
945
|
-
|
|
946
|
-
@Value("${jwt.expiration}")
|
|
947
|
-
private long jwtExpiration;
|
|
948
|
-
|
|
949
|
-
public String generateToken(Authentication authentication) {
|
|
950
|
-
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
951
|
-
|
|
952
|
-
return Jwts.builder()
|
|
953
|
-
.setSubject(userDetails.getUsername())
|
|
954
|
-
.claim("roles", userDetails.getAuthorities().stream()
|
|
955
|
-
.map(GrantedAuthority::getAuthority)
|
|
956
|
-
.collect(Collectors.toList()))
|
|
957
|
-
.setIssuedAt(new Date())
|
|
958
|
-
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
|
|
959
|
-
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
|
960
|
-
.compact();
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
public boolean validateToken(String token) {
|
|
964
|
-
try {
|
|
965
|
-
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
|
|
966
|
-
return true;
|
|
967
|
-
} catch (JwtException | IllegalArgumentException e) {
|
|
968
|
-
return false;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
public Authentication getAuthentication(String token) {
|
|
973
|
-
Claims claims = Jwts.parser()
|
|
974
|
-
.setSigningKey(jwtSecret)
|
|
975
|
-
.parseClaimsJws(token)
|
|
976
|
-
.getBody();
|
|
977
|
-
|
|
978
|
-
List<String> roles = claims.get("roles", List.class);
|
|
979
|
-
List<GrantedAuthority> authorities = roles.stream()
|
|
980
|
-
.map(SimpleGrantedAuthority::new)
|
|
981
|
-
.collect(Collectors.toList());
|
|
982
|
-
|
|
983
|
-
User principal = new User(claims.getSubject(), "", authorities);
|
|
984
|
-
return new UsernamePasswordAuthenticationToken(
|
|
985
|
-
principal, token, authorities
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
</details>
|
|
992
|
-
|
|
993
|
-
### レート制限とスロットリング
|
|
994
|
-
|
|
995
|
-
```plantuml
|
|
996
|
-
@startuml
|
|
997
|
-
title レート制限アルゴリズム
|
|
998
|
-
|
|
999
|
-
rectangle "Token Bucket" as token_bucket {
|
|
1000
|
-
note right
|
|
1001
|
-
・バケットにトークンを蓄積
|
|
1002
|
-
・リクエスト時にトークン消費
|
|
1003
|
-
・バースト対応可能
|
|
1004
|
-
|
|
1005
|
-
設定例:
|
|
1006
|
-
rate: 100/分
|
|
1007
|
-
burst: 20
|
|
1008
|
-
end note
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
rectangle "Sliding Window" as sliding_window {
|
|
1012
|
-
note right
|
|
1013
|
-
・時間枠内のリクエスト数をカウント
|
|
1014
|
-
・枠をスライドして計算
|
|
1015
|
-
・より正確な制限
|
|
1016
|
-
|
|
1017
|
-
設定例:
|
|
1018
|
-
window: 1分
|
|
1019
|
-
limit: 100
|
|
1020
|
-
end note
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
rectangle "Fixed Window" as fixed_window {
|
|
1024
|
-
note right
|
|
1025
|
-
・固定時間枠でカウント
|
|
1026
|
-
・シンプルな実装
|
|
1027
|
-
・境界でバーストの可能性
|
|
1028
|
-
|
|
1029
|
-
設定例:
|
|
1030
|
-
window: 1分
|
|
1031
|
-
limit: 100
|
|
1032
|
-
end note
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
@enduml
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
<details>
|
|
1039
|
-
<summary>Spring Cloud Gateway 設定例</summary>
|
|
1040
|
-
|
|
1041
|
-
```yaml
|
|
1042
|
-
# application.yml
|
|
1043
|
-
spring:
|
|
1044
|
-
cloud:
|
|
1045
|
-
gateway:
|
|
1046
|
-
routes:
|
|
1047
|
-
- id: sales-service
|
|
1048
|
-
uri: lb://sales-service
|
|
1049
|
-
predicates:
|
|
1050
|
-
- Path=/api/v1/sales/**
|
|
1051
|
-
filters:
|
|
1052
|
-
- name: RequestRateLimiter
|
|
1053
|
-
args:
|
|
1054
|
-
redis-rate-limiter.replenishRate: 100
|
|
1055
|
-
redis-rate-limiter.burstCapacity: 200
|
|
1056
|
-
key-resolver: "#{@userKeyResolver}"
|
|
1057
|
-
- name: CircuitBreaker
|
|
1058
|
-
args:
|
|
1059
|
-
name: salesCircuitBreaker
|
|
1060
|
-
fallbackUri: forward:/fallback/sales
|
|
1061
|
-
|
|
1062
|
-
- id: accounting-service
|
|
1063
|
-
uri: lb://accounting-service
|
|
1064
|
-
predicates:
|
|
1065
|
-
- Path=/api/v1/accounting/**
|
|
1066
|
-
filters:
|
|
1067
|
-
- name: RequestRateLimiter
|
|
1068
|
-
args:
|
|
1069
|
-
redis-rate-limiter.replenishRate: 50
|
|
1070
|
-
redis-rate-limiter.burstCapacity: 100
|
|
1071
|
-
|
|
1072
|
-
default-filters:
|
|
1073
|
-
- name: Retry
|
|
1074
|
-
args:
|
|
1075
|
-
retries: 3
|
|
1076
|
-
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
|
|
1077
|
-
methods: GET
|
|
1078
|
-
backoff:
|
|
1079
|
-
firstBackoff: 100ms
|
|
1080
|
-
maxBackoff: 500ms
|
|
1081
|
-
factor: 2
|
|
1082
|
-
```
|
|
1083
|
-
|
|
1084
|
-
```java
|
|
1085
|
-
// Key Resolver(ユーザー単位のレート制限)
|
|
1086
|
-
@Configuration
|
|
1087
|
-
public class RateLimiterConfig {
|
|
1088
|
-
|
|
1089
|
-
@Bean
|
|
1090
|
-
public KeyResolver userKeyResolver() {
|
|
1091
|
-
return exchange -> {
|
|
1092
|
-
String userId = exchange.getRequest()
|
|
1093
|
-
.getHeaders()
|
|
1094
|
-
.getFirst("X-User-Id");
|
|
1095
|
-
return Mono.just(userId != null ? userId : "anonymous");
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
@Bean
|
|
1100
|
-
public KeyResolver apiKeyResolver() {
|
|
1101
|
-
return exchange -> {
|
|
1102
|
-
String apiKey = exchange.getRequest()
|
|
1103
|
-
.getHeaders()
|
|
1104
|
-
.getFirst("X-API-Key");
|
|
1105
|
-
return Mono.just(apiKey != null ? apiKey : "default");
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
</details>
|
|
1112
|
-
|
|
1113
|
-
### ログ集約とモニタリング
|
|
1114
|
-
|
|
1115
|
-
```plantuml
|
|
1116
|
-
@startuml
|
|
1117
|
-
title 分散トレーシングとログ集約
|
|
1118
|
-
|
|
1119
|
-
rectangle "クライアント" as client
|
|
1120
|
-
|
|
1121
|
-
rectangle "API Gateway" as gateway {
|
|
1122
|
-
rectangle "トレースID生成" as trace_gen
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
rectangle "サービス群" as services {
|
|
1126
|
-
rectangle "販売サービス" as sales
|
|
1127
|
-
rectangle "在庫サービス" as inventory
|
|
1128
|
-
rectangle "会計サービス" as accounting
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
rectangle "観測基盤" as observability {
|
|
1132
|
-
database "ログ集約\n(ELK/Loki)" as logs
|
|
1133
|
-
database "メトリクス\n(Prometheus)" as metrics
|
|
1134
|
-
database "トレース\n(Jaeger/Zipkin)" as traces
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
rectangle "ダッシュボード" as dashboard {
|
|
1138
|
-
rectangle "Grafana" as grafana
|
|
1139
|
-
rectangle "Kibana" as kibana
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
client --> gateway : リクエスト
|
|
1143
|
-
gateway --> sales : traceId
|
|
1144
|
-
sales --> inventory : traceId
|
|
1145
|
-
sales --> accounting : traceId
|
|
1146
|
-
|
|
1147
|
-
gateway --> logs
|
|
1148
|
-
sales --> logs
|
|
1149
|
-
inventory --> logs
|
|
1150
|
-
accounting --> logs
|
|
1151
|
-
|
|
1152
|
-
gateway --> metrics
|
|
1153
|
-
sales --> metrics
|
|
1154
|
-
inventory --> metrics
|
|
1155
|
-
accounting --> metrics
|
|
1156
|
-
|
|
1157
|
-
gateway --> traces
|
|
1158
|
-
sales --> traces
|
|
1159
|
-
inventory --> traces
|
|
1160
|
-
accounting --> traces
|
|
1161
|
-
|
|
1162
|
-
logs --> kibana
|
|
1163
|
-
metrics --> grafana
|
|
1164
|
-
traces --> grafana
|
|
1165
|
-
|
|
1166
|
-
@enduml
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
<details>
|
|
1170
|
-
<summary>Java 実装例(Micrometer + OpenTelemetry)</summary>
|
|
1171
|
-
|
|
1172
|
-
```java
|
|
1173
|
-
// トレーシング設定
|
|
1174
|
-
@Configuration
|
|
1175
|
-
public class TracingConfig {
|
|
1176
|
-
|
|
1177
|
-
@Bean
|
|
1178
|
-
public Tracer tracer() {
|
|
1179
|
-
return GlobalOpenTelemetry.getTracer("sales-service");
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// カスタムメトリクス
|
|
1184
|
-
@Component
|
|
1185
|
-
public class OrderMetrics {
|
|
1186
|
-
private final MeterRegistry meterRegistry;
|
|
1187
|
-
private final Counter orderCreatedCounter;
|
|
1188
|
-
private final Timer orderProcessingTimer;
|
|
1189
|
-
|
|
1190
|
-
public OrderMetrics(MeterRegistry meterRegistry) {
|
|
1191
|
-
this.meterRegistry = meterRegistry;
|
|
1192
|
-
|
|
1193
|
-
this.orderCreatedCounter = Counter.builder("orders.created")
|
|
1194
|
-
.description("Number of orders created")
|
|
1195
|
-
.tag("service", "sales")
|
|
1196
|
-
.register(meterRegistry);
|
|
1197
|
-
|
|
1198
|
-
this.orderProcessingTimer = Timer.builder("orders.processing.time")
|
|
1199
|
-
.description("Order processing time")
|
|
1200
|
-
.tag("service", "sales")
|
|
1201
|
-
.register(meterRegistry);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
public void recordOrderCreated(String status) {
|
|
1205
|
-
orderCreatedCounter.increment();
|
|
1206
|
-
meterRegistry.counter("orders.created.by.status",
|
|
1207
|
-
"status", status).increment();
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
public void recordProcessingTime(long durationMs) {
|
|
1211
|
-
orderProcessingTimer.record(Duration.ofMillis(durationMs));
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// 構造化ログ
|
|
1216
|
-
@Slf4j
|
|
1217
|
-
@Service
|
|
1218
|
-
public class OrderService {
|
|
1219
|
-
|
|
1220
|
-
public Order createOrder(CreateOrderRequest request) {
|
|
1221
|
-
MDC.put("orderId", request.getOrderId());
|
|
1222
|
-
MDC.put("customerId", request.getCustomerId());
|
|
1223
|
-
|
|
1224
|
-
try {
|
|
1225
|
-
log.info("Creating order", kv("action", "create_order_start"));
|
|
1226
|
-
|
|
1227
|
-
Order order = processOrder(request);
|
|
1228
|
-
|
|
1229
|
-
log.info("Order created successfully",
|
|
1230
|
-
kv("action", "create_order_complete"),
|
|
1231
|
-
kv("totalAmount", order.getTotalAmount()));
|
|
1232
|
-
|
|
1233
|
-
return order;
|
|
1234
|
-
} catch (Exception e) {
|
|
1235
|
-
log.error("Failed to create order",
|
|
1236
|
-
kv("action", "create_order_failed"),
|
|
1237
|
-
kv("error", e.getMessage()), e);
|
|
1238
|
-
throw e;
|
|
1239
|
-
} finally {
|
|
1240
|
-
MDC.clear();
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
```
|
|
1245
|
-
|
|
1246
|
-
</details>
|
|
1247
|
-
|
|
1248
|
-
---
|
|
1249
|
-
|
|
1250
|
-
## 38.4 API インテグレーションテスト
|
|
1251
|
-
|
|
1252
|
-
### テストコンテナによる統合テスト環境
|
|
1253
|
-
|
|
1254
|
-
```plantuml
|
|
1255
|
-
@startuml
|
|
1256
|
-
title テストコンテナアーキテクチャ
|
|
1257
|
-
|
|
1258
|
-
rectangle "テスト実行環境" as test_env {
|
|
1259
|
-
rectangle "JUnit 5" as junit
|
|
1260
|
-
rectangle "テストクラス" as test_class
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
rectangle "Testcontainers" as testcontainers {
|
|
1264
|
-
rectangle "PostgreSQL\nコンテナ" as postgres
|
|
1265
|
-
rectangle "RabbitMQ\nコンテナ" as rabbitmq
|
|
1266
|
-
rectangle "Redis\nコンテナ" as redis
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
rectangle "アプリケーション" as app {
|
|
1270
|
-
rectangle "Spring Boot\nアプリケーション" as spring
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
test_class --> spring : HTTP リクエスト
|
|
1274
|
-
spring --> postgres : データアクセス
|
|
1275
|
-
spring --> rabbitmq : メッセージング
|
|
1276
|
-
spring --> redis : キャッシュ
|
|
1277
|
-
|
|
1278
|
-
junit --> testcontainers : コンテナ起動
|
|
1279
|
-
testcontainers --> postgres
|
|
1280
|
-
testcontainers --> rabbitmq
|
|
1281
|
-
testcontainers --> redis
|
|
1282
|
-
|
|
1283
|
-
note bottom of testcontainers
|
|
1284
|
-
【Testcontainersの利点】
|
|
1285
|
-
・本番同等の環境でテスト
|
|
1286
|
-
・テスト間の分離
|
|
1287
|
-
・CI/CDでの再現性
|
|
1288
|
-
・自動クリーンアップ
|
|
1289
|
-
end note
|
|
1290
|
-
|
|
1291
|
-
@enduml
|
|
1292
|
-
```
|
|
1293
|
-
|
|
1294
|
-
<details>
|
|
1295
|
-
<summary>Java 実装例</summary>
|
|
1296
|
-
|
|
1297
|
-
```java
|
|
1298
|
-
// テストコンテナ設定
|
|
1299
|
-
@Testcontainers
|
|
1300
|
-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1301
|
-
@ActiveProfiles("test")
|
|
1302
|
-
public abstract class IntegrationTestBase {
|
|
1303
|
-
|
|
1304
|
-
@Container
|
|
1305
|
-
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
|
|
1306
|
-
.withDatabaseName("testdb")
|
|
1307
|
-
.withUsername("test")
|
|
1308
|
-
.withPassword("test");
|
|
1309
|
-
|
|
1310
|
-
@Container
|
|
1311
|
-
static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.12-management");
|
|
1312
|
-
|
|
1313
|
-
@DynamicPropertySource
|
|
1314
|
-
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1315
|
-
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1316
|
-
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1317
|
-
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1318
|
-
registry.add("spring.rabbitmq.host", rabbitmq::getHost);
|
|
1319
|
-
registry.add("spring.rabbitmq.port", rabbitmq::getAmqpPort);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
@Autowired
|
|
1323
|
-
protected TestRestTemplate restTemplate;
|
|
1324
|
-
|
|
1325
|
-
@Autowired
|
|
1326
|
-
protected JdbcTemplate jdbcTemplate;
|
|
1327
|
-
|
|
1328
|
-
@BeforeEach
|
|
1329
|
-
void setUp() {
|
|
1330
|
-
// テストデータのクリーンアップ
|
|
1331
|
-
cleanupTestData();
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
protected void cleanupTestData() {
|
|
1335
|
-
jdbcTemplate.execute("TRUNCATE TABLE 受注明細 CASCADE");
|
|
1336
|
-
jdbcTemplate.execute("TRUNCATE TABLE 受注 CASCADE");
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
</details>
|
|
1342
|
-
|
|
1343
|
-
### REST API エンドポイントのテスト
|
|
1344
|
-
|
|
1345
|
-
```plantuml
|
|
1346
|
-
@startuml
|
|
1347
|
-
title API テストの構造
|
|
1348
|
-
|
|
1349
|
-
' ノード間の間隔を調整
|
|
1350
|
-
skinparam nodesep 50
|
|
1351
|
-
skinparam ranksep 50
|
|
1352
|
-
|
|
1353
|
-
rectangle "== Given(前提条件)\n\n・テストデータ準備\n・認証トークン取得\n・モックの設定" as given
|
|
1354
|
-
|
|
1355
|
-
rectangle "== When(操作)\n\n・APIエンドポイント呼び出し\n・リクエストボディ設定\n・ヘッダー設定" as when
|
|
1356
|
-
|
|
1357
|
-
rectangle "== Then(検証)\n\n・ステータスコード確認\n・レスポンスボディ検証\n・データベース状態確認\n・イベント発行確認" as then
|
|
1358
|
-
|
|
1359
|
-
given --> when
|
|
1360
|
-
when --> then
|
|
1361
|
-
|
|
1362
|
-
@enduml
|
|
1363
|
-
```
|
|
1364
|
-
|
|
1365
|
-
<details>
|
|
1366
|
-
<summary>Java 実装例</summary>
|
|
1367
|
-
|
|
1368
|
-
```java
|
|
1369
|
-
// 受注API統合テスト
|
|
1370
|
-
class OrderApiIntegrationTest extends IntegrationTestBase {
|
|
1371
|
-
|
|
1372
|
-
@Autowired
|
|
1373
|
-
private CustomerRepository customerRepository;
|
|
1374
|
-
|
|
1375
|
-
@Autowired
|
|
1376
|
-
private ProductRepository productRepository;
|
|
1377
|
-
|
|
1378
|
-
private String authToken;
|
|
1379
|
-
private Customer testCustomer;
|
|
1380
|
-
private Product testProduct;
|
|
1381
|
-
|
|
1382
|
-
@BeforeEach
|
|
1383
|
-
void setUpTestData() {
|
|
1384
|
-
// 認証トークン取得
|
|
1385
|
-
authToken = getAuthToken("test-user", "password");
|
|
1386
|
-
|
|
1387
|
-
// テストデータ準備
|
|
1388
|
-
testCustomer = customerRepository.save(
|
|
1389
|
-
new Customer("CUS-001", "テスト顧客", "test@example.com")
|
|
1390
|
-
);
|
|
1391
|
-
testProduct = productRepository.save(
|
|
1392
|
-
new Product("PRD-001", "テスト商品", new BigDecimal("1000"))
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
@Test
|
|
1397
|
-
@DisplayName("受注登録: 正常系")
|
|
1398
|
-
void createOrder_Success() {
|
|
1399
|
-
// Given
|
|
1400
|
-
CreateOrderRequest request = CreateOrderRequest.builder()
|
|
1401
|
-
.customerId(testCustomer.getCustomerId())
|
|
1402
|
-
.orderDate(LocalDate.now())
|
|
1403
|
-
.lines(List.of(
|
|
1404
|
-
OrderLineRequest.builder()
|
|
1405
|
-
.productId(testProduct.getProductId())
|
|
1406
|
-
.quantity(10)
|
|
1407
|
-
.build()
|
|
1408
|
-
))
|
|
1409
|
-
.build();
|
|
1410
|
-
|
|
1411
|
-
// When
|
|
1412
|
-
ResponseEntity<OrderResponse> response = restTemplate.exchange(
|
|
1413
|
-
"/api/v1/sales/orders",
|
|
1414
|
-
HttpMethod.POST,
|
|
1415
|
-
new HttpEntity<>(request, createAuthHeaders()),
|
|
1416
|
-
OrderResponse.class
|
|
1417
|
-
);
|
|
1418
|
-
|
|
1419
|
-
// Then
|
|
1420
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1421
|
-
assertThat(response.getBody()).isNotNull();
|
|
1422
|
-
assertThat(response.getBody().getOrderId()).isNotBlank();
|
|
1423
|
-
assertThat(response.getBody().getStatus()).isEqualTo("DRAFT");
|
|
1424
|
-
assertThat(response.getBody().getTotalAmount())
|
|
1425
|
-
.isEqualByComparingTo(new BigDecimal("10000"));
|
|
1426
|
-
|
|
1427
|
-
// データベース状態確認
|
|
1428
|
-
Order savedOrder = orderRepository.findById(
|
|
1429
|
-
response.getBody().getOrderId()
|
|
1430
|
-
).orElseThrow();
|
|
1431
|
-
assertThat(savedOrder.getCustomerId()).isEqualTo(testCustomer.getCustomerId());
|
|
1432
|
-
assertThat(savedOrder.getLines()).hasSize(1);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
@Test
|
|
1436
|
-
@DisplayName("受注登録: バリデーションエラー")
|
|
1437
|
-
void createOrder_ValidationError() {
|
|
1438
|
-
// Given - 明細なしのリクエスト
|
|
1439
|
-
CreateOrderRequest request = CreateOrderRequest.builder()
|
|
1440
|
-
.customerId(testCustomer.getCustomerId())
|
|
1441
|
-
.orderDate(LocalDate.now())
|
|
1442
|
-
.lines(List.of()) // 空の明細
|
|
1443
|
-
.build();
|
|
1444
|
-
|
|
1445
|
-
// When
|
|
1446
|
-
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1447
|
-
"/api/v1/sales/orders",
|
|
1448
|
-
HttpMethod.POST,
|
|
1449
|
-
new HttpEntity<>(request, createAuthHeaders()),
|
|
1450
|
-
ProblemDetail.class
|
|
1451
|
-
);
|
|
1452
|
-
|
|
1453
|
-
// Then
|
|
1454
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
|
1455
|
-
assertThat(response.getBody().getTitle()).isEqualTo("Validation Error");
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
@Test
|
|
1459
|
-
@DisplayName("受注一覧取得: ページング")
|
|
1460
|
-
void getOrders_Paging() {
|
|
1461
|
-
// Given - 複数の受注を作成
|
|
1462
|
-
for (int i = 0; i < 25; i++) {
|
|
1463
|
-
createTestOrder("ORD-" + String.format("%03d", i));
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// When
|
|
1467
|
-
ResponseEntity<PagedResponse<OrderSummary>> response = restTemplate.exchange(
|
|
1468
|
-
"/api/v1/sales/orders?page=0&size=10",
|
|
1469
|
-
HttpMethod.GET,
|
|
1470
|
-
new HttpEntity<>(createAuthHeaders()),
|
|
1471
|
-
new ParameterizedTypeReference<>() {}
|
|
1472
|
-
);
|
|
1473
|
-
|
|
1474
|
-
// Then
|
|
1475
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1476
|
-
assertThat(response.getBody().getContent()).hasSize(10);
|
|
1477
|
-
assertThat(response.getBody().getTotalElements()).isEqualTo(25);
|
|
1478
|
-
assertThat(response.getBody().getTotalPages()).isEqualTo(3);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
private HttpHeaders createAuthHeaders() {
|
|
1482
|
-
HttpHeaders headers = new HttpHeaders();
|
|
1483
|
-
headers.setBearerAuth(authToken);
|
|
1484
|
-
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
1485
|
-
return headers;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
```
|
|
1489
|
-
|
|
1490
|
-
</details>
|
|
1491
|
-
|
|
1492
|
-
### サービス間連携テスト
|
|
1493
|
-
|
|
1494
|
-
```plantuml
|
|
1495
|
-
@startuml
|
|
1496
|
-
title サービス間連携テストのアプローチ
|
|
1497
|
-
|
|
1498
|
-
' ボックスの定義
|
|
1499
|
-
rectangle "Contract Testing" as contract
|
|
1500
|
-
note right of contract
|
|
1501
|
-
・Pact / Spring Cloud Contract
|
|
1502
|
-
・プロバイダーとコンシューマーの契約
|
|
1503
|
-
・独立したテスト実行
|
|
1504
|
-
end note
|
|
1505
|
-
|
|
1506
|
-
rectangle "Component Testing" as component
|
|
1507
|
-
note right of component
|
|
1508
|
-
・WireMockで外部サービスをモック
|
|
1509
|
-
・単一サービスの統合テスト
|
|
1510
|
-
・高速なフィードバック
|
|
1511
|
-
end note
|
|
1512
|
-
|
|
1513
|
-
rectangle "End-to-End Testing" as e2e
|
|
1514
|
-
note right of e2e
|
|
1515
|
-
・全サービスを起動
|
|
1516
|
-
・Docker Composeで環境構築
|
|
1517
|
-
・本番同等のシナリオテスト
|
|
1518
|
-
end note
|
|
1519
|
-
|
|
1520
|
-
' 配置(上から下に並べる)
|
|
1521
|
-
contract -[hidden]down-> component
|
|
1522
|
-
component -[hidden]down-> e2e
|
|
1523
|
-
|
|
1524
|
-
@enduml
|
|
1525
|
-
```
|
|
1526
|
-
|
|
1527
|
-
<details>
|
|
1528
|
-
<summary>Java 実装例(WireMock)</summary>
|
|
1529
|
-
|
|
1530
|
-
```java
|
|
1531
|
-
// WireMockを使用したサービス間連携テスト
|
|
1532
|
-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1533
|
-
@AutoConfigureWireMock(port = 0)
|
|
1534
|
-
class OrderServiceWithInventoryTest {
|
|
1535
|
-
|
|
1536
|
-
@Autowired
|
|
1537
|
-
private TestRestTemplate restTemplate;
|
|
1538
|
-
|
|
1539
|
-
@Value("${wiremock.server.port}")
|
|
1540
|
-
private int wireMockPort;
|
|
1541
|
-
|
|
1542
|
-
@BeforeEach
|
|
1543
|
-
void setUp() {
|
|
1544
|
-
// 在庫サービスのモック設定
|
|
1545
|
-
stubFor(get(urlPathMatching("/api/v1/inventory/.*"))
|
|
1546
|
-
.willReturn(aResponse()
|
|
1547
|
-
.withStatus(200)
|
|
1548
|
-
.withHeader("Content-Type", "application/json")
|
|
1549
|
-
.withBody("""
|
|
1550
|
-
{
|
|
1551
|
-
"productId": "PRD-001",
|
|
1552
|
-
"quantity": 100,
|
|
1553
|
-
"allocatedQuantity": 0,
|
|
1554
|
-
"availableQuantity": 100
|
|
1555
|
-
}
|
|
1556
|
-
""")));
|
|
1557
|
-
|
|
1558
|
-
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1559
|
-
.willReturn(aResponse()
|
|
1560
|
-
.withStatus(200)
|
|
1561
|
-
.withHeader("Content-Type", "application/json")
|
|
1562
|
-
.withBody("""
|
|
1563
|
-
{
|
|
1564
|
-
"success": true,
|
|
1565
|
-
"allocationId": "ALLOC-001"
|
|
1566
|
-
}
|
|
1567
|
-
""")));
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
@Test
|
|
1571
|
-
@DisplayName("受注確定: 在庫引当成功")
|
|
1572
|
-
void confirmOrder_InventoryAllocated() {
|
|
1573
|
-
// Given
|
|
1574
|
-
String orderId = createDraftOrder();
|
|
1575
|
-
|
|
1576
|
-
// When
|
|
1577
|
-
ResponseEntity<OrderResponse> response = restTemplate.exchange(
|
|
1578
|
-
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1579
|
-
HttpMethod.POST,
|
|
1580
|
-
new HttpEntity<>(createAuthHeaders()),
|
|
1581
|
-
OrderResponse.class,
|
|
1582
|
-
orderId
|
|
1583
|
-
);
|
|
1584
|
-
|
|
1585
|
-
// Then
|
|
1586
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1587
|
-
assertThat(response.getBody().getStatus()).isEqualTo("CONFIRMED");
|
|
1588
|
-
|
|
1589
|
-
// 在庫サービスへの呼び出しを検証
|
|
1590
|
-
verify(postRequestedFor(urlPathEqualTo("/api/v1/inventory/allocate"))
|
|
1591
|
-
.withRequestBody(matchingJsonPath("$.productId", equalTo("PRD-001")))
|
|
1592
|
-
.withRequestBody(matchingJsonPath("$.quantity", equalTo("10"))));
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
@Test
|
|
1596
|
-
@DisplayName("受注確定: 在庫不足")
|
|
1597
|
-
void confirmOrder_InsufficientStock() {
|
|
1598
|
-
// Given - 在庫不足のレスポンスを設定
|
|
1599
|
-
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1600
|
-
.willReturn(aResponse()
|
|
1601
|
-
.withStatus(200)
|
|
1602
|
-
.withBody("""
|
|
1603
|
-
{
|
|
1604
|
-
"success": false,
|
|
1605
|
-
"errorMessage": "Insufficient stock"
|
|
1606
|
-
}
|
|
1607
|
-
""")));
|
|
1608
|
-
|
|
1609
|
-
String orderId = createDraftOrder();
|
|
1610
|
-
|
|
1611
|
-
// When
|
|
1612
|
-
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1613
|
-
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1614
|
-
HttpMethod.POST,
|
|
1615
|
-
new HttpEntity<>(createAuthHeaders()),
|
|
1616
|
-
ProblemDetail.class,
|
|
1617
|
-
orderId
|
|
1618
|
-
);
|
|
1619
|
-
|
|
1620
|
-
// Then
|
|
1621
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
|
|
1622
|
-
assertThat(response.getBody().getDetail()).contains("在庫不足");
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
@Test
|
|
1626
|
-
@DisplayName("受注確定: 在庫サービスタイムアウト")
|
|
1627
|
-
void confirmOrder_InventoryServiceTimeout() {
|
|
1628
|
-
// Given - タイムアウトをシミュレート
|
|
1629
|
-
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1630
|
-
.willReturn(aResponse()
|
|
1631
|
-
.withFixedDelay(5000) // 5秒遅延
|
|
1632
|
-
.withStatus(200)));
|
|
1633
|
-
|
|
1634
|
-
String orderId = createDraftOrder();
|
|
1635
|
-
|
|
1636
|
-
// When
|
|
1637
|
-
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1638
|
-
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1639
|
-
HttpMethod.POST,
|
|
1640
|
-
new HttpEntity<>(createAuthHeaders()),
|
|
1641
|
-
ProblemDetail.class,
|
|
1642
|
-
orderId
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
// Then - サーキットブレーカーによるフォールバック
|
|
1646
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
```
|
|
1650
|
-
|
|
1651
|
-
</details>
|
|
1652
|
-
|
|
1653
|
-
---
|
|
1654
|
-
|
|
1655
|
-
## 38.5 まとめ
|
|
1656
|
-
|
|
1657
|
-
本章では、API 設計とサービス連携のパターンについて解説しました。
|
|
1658
|
-
|
|
1659
|
-
### 学んだこと
|
|
1660
|
-
|
|
1661
|
-
1. **API 設計の原則**
|
|
1662
|
-
|
|
1663
|
-
- RESTful API の制約と設計原則
|
|
1664
|
-
- リソース指向設計(名詞ベース、HTTP メソッド)
|
|
1665
|
-
- バージョニング戦略(URI パス推奨)
|
|
1666
|
-
|
|
1667
|
-
2. **サービス間通信**
|
|
1668
|
-
|
|
1669
|
-
- 同期通信(REST、gRPC)の使い分け
|
|
1670
|
-
- 非同期通信(メッセージキュー)
|
|
1671
|
-
- サーキットブレーカーによる障害対策
|
|
1672
|
-
|
|
1673
|
-
3. **API ゲートウェイ**
|
|
1674
|
-
|
|
1675
|
-
- 認証・認可の一元化(JWT)
|
|
1676
|
-
- レート制限とスロットリング
|
|
1677
|
-
- ログ集約とモニタリング
|
|
1678
|
-
|
|
1679
|
-
4. **API インテグレーションテスト**
|
|
1680
|
-
|
|
1681
|
-
- テストコンテナによる統合テスト環境
|
|
1682
|
-
- REST API エンドポイントのテスト
|
|
1683
|
-
- サービス間連携テスト(WireMock)
|
|
1684
|
-
|
|
1685
|
-
### API 設計チェックリスト
|
|
1686
|
-
|
|
1687
|
-
- [ ] リソースは名詞(複数形)で命名されているか
|
|
1688
|
-
- [ ] HTTP メソッドが正しく使用されているか
|
|
1689
|
-
- [ ] エラーレスポンスは Problem Detail 形式か
|
|
1690
|
-
- [ ] バージョニング戦略が決定されているか
|
|
1691
|
-
- [ ] 認証・認可が適切に実装されているか
|
|
1692
|
-
- [ ] レート制限が設定されているか
|
|
1693
|
-
- [ ] サーキットブレーカーが導入されているか
|
|
1694
|
-
- [ ] 統合テストが整備されているか
|
|
1695
|
-
|
|
1696
|
-
### 次章の予告
|
|
1697
|
-
|
|
1698
|
-
第39章では、データ連携の実装パターンについて解説します。バッチ連携、リアルタイム連携、連携テーブルの設計など、具体的な実装パターンを学びます。
|
|
1
|
+
# 第38章:API 設計とサービス連携
|
|
2
|
+
|
|
3
|
+
本章では、基幹業務システムにおける API 設計の原則と、サービス間連携のパターンについて解説します。RESTful API の設計、サービス間通信の方式、API ゲートウェイの活用、そしてインテグレーションテストの実践方法を学びます。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 38.1 API 設計の原則
|
|
8
|
+
|
|
9
|
+
### RESTful API の設計
|
|
10
|
+
|
|
11
|
+
REST(Representational State Transfer)は、Web API 設計の標準的なアーキテクチャスタイルです。
|
|
12
|
+
|
|
13
|
+
```plantuml
|
|
14
|
+
@startuml
|
|
15
|
+
title RESTful API の基本原則
|
|
16
|
+
|
|
17
|
+
rectangle "REST の制約" as constraints {
|
|
18
|
+
rectangle "クライアント-サーバー" as cs {
|
|
19
|
+
note right
|
|
20
|
+
関心の分離
|
|
21
|
+
独立した進化
|
|
22
|
+
end note
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
rectangle "ステートレス" as stateless {
|
|
26
|
+
note right
|
|
27
|
+
各リクエストは独立
|
|
28
|
+
セッション状態を保持しない
|
|
29
|
+
end note
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
rectangle "キャッシュ可能" as cacheable {
|
|
33
|
+
note right
|
|
34
|
+
レスポンスにキャッシュ可否を明示
|
|
35
|
+
パフォーマンス向上
|
|
36
|
+
end note
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
rectangle "統一インターフェース" as uniform {
|
|
40
|
+
note right
|
|
41
|
+
リソース識別(URI)
|
|
42
|
+
表現によるリソース操作
|
|
43
|
+
自己記述メッセージ
|
|
44
|
+
HATEOAS
|
|
45
|
+
end note
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
rectangle "階層化システム" as layered {
|
|
49
|
+
note right
|
|
50
|
+
中間サーバーの追加が可能
|
|
51
|
+
ロードバランサー、キャッシュ等
|
|
52
|
+
end note
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@enduml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### HTTP メソッドとリソース操作
|
|
60
|
+
|
|
61
|
+
| HTTP メソッド | 操作 | 冪等性 | 安全性 | 使用例 |
|
|
62
|
+
|-------------|-----|-------|-------|-------|
|
|
63
|
+
| GET | 取得 | Yes | Yes | リソースの参照 |
|
|
64
|
+
| POST | 作成 | No | No | 新規リソース作成 |
|
|
65
|
+
| PUT | 置換 | Yes | No | リソース全体の更新 |
|
|
66
|
+
| PATCH | 部分更新 | No | No | リソースの一部更新 |
|
|
67
|
+
| DELETE | 削除 | Yes | No | リソースの削除 |
|
|
68
|
+
|
|
69
|
+
### リソース指向設計
|
|
70
|
+
|
|
71
|
+
API はリソース(名詞)を中心に設計し、操作は HTTP メソッドで表現します。
|
|
72
|
+
|
|
73
|
+
```plantuml
|
|
74
|
+
@startuml
|
|
75
|
+
title リソース指向 API 設計
|
|
76
|
+
|
|
77
|
+
package "販売管理 API" as sales_api {
|
|
78
|
+
rectangle "/orders" as orders {
|
|
79
|
+
rectangle "GET /orders" as get_orders
|
|
80
|
+
rectangle "POST /orders" as post_order
|
|
81
|
+
rectangle "GET /orders/{id}" as get_order
|
|
82
|
+
rectangle "PUT /orders/{id}" as put_order
|
|
83
|
+
rectangle "DELETE /orders/{id}" as delete_order
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rectangle "/orders/{id}/lines" as order_lines {
|
|
87
|
+
rectangle "GET /orders/{id}/lines" as get_lines
|
|
88
|
+
rectangle "POST /orders/{id}/lines" as post_line
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
rectangle "/customers" as customers {
|
|
92
|
+
rectangle "GET /customers" as get_customers
|
|
93
|
+
rectangle "POST /customers" as post_customer
|
|
94
|
+
rectangle "GET /customers/{id}" as get_customer
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
rectangle "/customers/{id}/orders" as customer_orders {
|
|
98
|
+
rectangle "GET /customers/{id}/orders" as get_customer_orders
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
note bottom of sales_api
|
|
103
|
+
【設計原則】
|
|
104
|
+
・リソースは名詞(複数形)
|
|
105
|
+
・操作はHTTPメソッドで表現
|
|
106
|
+
・階層構造で関連を表現
|
|
107
|
+
・クエリパラメータでフィルタリング
|
|
108
|
+
end note
|
|
109
|
+
|
|
110
|
+
@enduml
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### 基幹業務システムの API エンドポイント設計
|
|
114
|
+
|
|
115
|
+
```plantuml
|
|
116
|
+
@startuml
|
|
117
|
+
title 基幹業務システム API エンドポイント
|
|
118
|
+
|
|
119
|
+
package "販売管理 API" as sales {
|
|
120
|
+
rectangle "/api/v1/sales" as sales_base {
|
|
121
|
+
rectangle "受注: /orders"
|
|
122
|
+
rectangle "出荷: /shipments"
|
|
123
|
+
rectangle "売上: /sales"
|
|
124
|
+
rectangle "請求: /invoices"
|
|
125
|
+
rectangle "入金: /payments"
|
|
126
|
+
}
|
|
127
|
+
rectangle "/api/v1/sales/masters" as sales_masters {
|
|
128
|
+
rectangle "顧客: /customers"
|
|
129
|
+
rectangle "商品: /products"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
package "財務会計 API" as accounting {
|
|
134
|
+
rectangle "/api/v1/accounting" as acc_base {
|
|
135
|
+
rectangle "仕訳: /journals"
|
|
136
|
+
rectangle "残高: /balances"
|
|
137
|
+
rectangle "試算表: /trial-balances"
|
|
138
|
+
}
|
|
139
|
+
rectangle "/api/v1/accounting/masters" as acc_masters {
|
|
140
|
+
rectangle "勘定科目: /accounts"
|
|
141
|
+
rectangle "補助科目: /sub-accounts"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
package "生産管理 API" as production {
|
|
146
|
+
rectangle "/api/v1/production" as prod_base {
|
|
147
|
+
rectangle "製造指示: /work-orders"
|
|
148
|
+
rectangle "発注: /purchase-orders"
|
|
149
|
+
rectangle "在庫: /inventory"
|
|
150
|
+
}
|
|
151
|
+
rectangle "/api/v1/production/masters" as prod_masters {
|
|
152
|
+
rectangle "品目: /items"
|
|
153
|
+
rectangle "BOM: /bom"
|
|
154
|
+
rectangle "工程: /processes"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@enduml
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>OpenAPI 定義例</summary>
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
openapi: 3.0.3
|
|
166
|
+
info:
|
|
167
|
+
title: 販売管理 API
|
|
168
|
+
version: 1.0.0
|
|
169
|
+
description: 基幹業務システム - 販売管理 API
|
|
170
|
+
|
|
171
|
+
servers:
|
|
172
|
+
- url: https://api.example.com/api/v1/sales
|
|
173
|
+
description: Production server
|
|
174
|
+
|
|
175
|
+
paths:
|
|
176
|
+
/orders:
|
|
177
|
+
get:
|
|
178
|
+
summary: 受注一覧取得
|
|
179
|
+
operationId: getOrders
|
|
180
|
+
parameters:
|
|
181
|
+
- name: customerId
|
|
182
|
+
in: query
|
|
183
|
+
schema:
|
|
184
|
+
type: string
|
|
185
|
+
- name: status
|
|
186
|
+
in: query
|
|
187
|
+
schema:
|
|
188
|
+
type: string
|
|
189
|
+
enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
|
|
190
|
+
- name: fromDate
|
|
191
|
+
in: query
|
|
192
|
+
schema:
|
|
193
|
+
type: string
|
|
194
|
+
format: date
|
|
195
|
+
- name: toDate
|
|
196
|
+
in: query
|
|
197
|
+
schema:
|
|
198
|
+
type: string
|
|
199
|
+
format: date
|
|
200
|
+
- name: page
|
|
201
|
+
in: query
|
|
202
|
+
schema:
|
|
203
|
+
type: integer
|
|
204
|
+
default: 0
|
|
205
|
+
- name: size
|
|
206
|
+
in: query
|
|
207
|
+
schema:
|
|
208
|
+
type: integer
|
|
209
|
+
default: 20
|
|
210
|
+
responses:
|
|
211
|
+
'200':
|
|
212
|
+
description: 成功
|
|
213
|
+
content:
|
|
214
|
+
application/json:
|
|
215
|
+
schema:
|
|
216
|
+
$ref: '#/components/schemas/OrderListResponse'
|
|
217
|
+
|
|
218
|
+
post:
|
|
219
|
+
summary: 受注登録
|
|
220
|
+
operationId: createOrder
|
|
221
|
+
requestBody:
|
|
222
|
+
required: true
|
|
223
|
+
content:
|
|
224
|
+
application/json:
|
|
225
|
+
schema:
|
|
226
|
+
$ref: '#/components/schemas/CreateOrderRequest'
|
|
227
|
+
responses:
|
|
228
|
+
'201':
|
|
229
|
+
description: 作成成功
|
|
230
|
+
content:
|
|
231
|
+
application/json:
|
|
232
|
+
schema:
|
|
233
|
+
$ref: '#/components/schemas/OrderResponse'
|
|
234
|
+
'400':
|
|
235
|
+
description: バリデーションエラー
|
|
236
|
+
content:
|
|
237
|
+
application/problem+json:
|
|
238
|
+
schema:
|
|
239
|
+
$ref: '#/components/schemas/ProblemDetail'
|
|
240
|
+
|
|
241
|
+
/orders/{orderId}:
|
|
242
|
+
get:
|
|
243
|
+
summary: 受注詳細取得
|
|
244
|
+
operationId: getOrder
|
|
245
|
+
parameters:
|
|
246
|
+
- name: orderId
|
|
247
|
+
in: path
|
|
248
|
+
required: true
|
|
249
|
+
schema:
|
|
250
|
+
type: string
|
|
251
|
+
responses:
|
|
252
|
+
'200':
|
|
253
|
+
description: 成功
|
|
254
|
+
content:
|
|
255
|
+
application/json:
|
|
256
|
+
schema:
|
|
257
|
+
$ref: '#/components/schemas/OrderResponse'
|
|
258
|
+
'404':
|
|
259
|
+
description: 受注が見つからない
|
|
260
|
+
|
|
261
|
+
components:
|
|
262
|
+
schemas:
|
|
263
|
+
OrderResponse:
|
|
264
|
+
type: object
|
|
265
|
+
properties:
|
|
266
|
+
orderId:
|
|
267
|
+
type: string
|
|
268
|
+
customerId:
|
|
269
|
+
type: string
|
|
270
|
+
customerName:
|
|
271
|
+
type: string
|
|
272
|
+
orderDate:
|
|
273
|
+
type: string
|
|
274
|
+
format: date
|
|
275
|
+
status:
|
|
276
|
+
type: string
|
|
277
|
+
enum: [DRAFT, CONFIRMED, SHIPPED, COMPLETED]
|
|
278
|
+
totalAmount:
|
|
279
|
+
type: number
|
|
280
|
+
taxAmount:
|
|
281
|
+
type: number
|
|
282
|
+
lines:
|
|
283
|
+
type: array
|
|
284
|
+
items:
|
|
285
|
+
$ref: '#/components/schemas/OrderLineResponse'
|
|
286
|
+
_links:
|
|
287
|
+
$ref: '#/components/schemas/Links'
|
|
288
|
+
|
|
289
|
+
CreateOrderRequest:
|
|
290
|
+
type: object
|
|
291
|
+
required:
|
|
292
|
+
- customerId
|
|
293
|
+
- orderDate
|
|
294
|
+
- lines
|
|
295
|
+
properties:
|
|
296
|
+
customerId:
|
|
297
|
+
type: string
|
|
298
|
+
orderDate:
|
|
299
|
+
type: string
|
|
300
|
+
format: date
|
|
301
|
+
requestedDeliveryDate:
|
|
302
|
+
type: string
|
|
303
|
+
format: date
|
|
304
|
+
lines:
|
|
305
|
+
type: array
|
|
306
|
+
items:
|
|
307
|
+
$ref: '#/components/schemas/CreateOrderLineRequest'
|
|
308
|
+
minItems: 1
|
|
309
|
+
|
|
310
|
+
ProblemDetail:
|
|
311
|
+
type: object
|
|
312
|
+
properties:
|
|
313
|
+
type:
|
|
314
|
+
type: string
|
|
315
|
+
title:
|
|
316
|
+
type: string
|
|
317
|
+
status:
|
|
318
|
+
type: integer
|
|
319
|
+
detail:
|
|
320
|
+
type: string
|
|
321
|
+
instance:
|
|
322
|
+
type: string
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
</details>
|
|
326
|
+
|
|
327
|
+
### バージョニング戦略
|
|
328
|
+
|
|
329
|
+
API の互換性を維持しながら進化させるためのバージョニング戦略を検討します。
|
|
330
|
+
|
|
331
|
+
```plantuml
|
|
332
|
+
@startuml
|
|
333
|
+
title API バージョニング戦略の比較
|
|
334
|
+
|
|
335
|
+
rectangle "URI パスバージョニング" as uri {
|
|
336
|
+
note right
|
|
337
|
+
/api/v1/orders
|
|
338
|
+
/api/v2/orders
|
|
339
|
+
|
|
340
|
+
【メリット】
|
|
341
|
+
・明確で分かりやすい
|
|
342
|
+
・キャッシュしやすい
|
|
343
|
+
|
|
344
|
+
【デメリット】
|
|
345
|
+
・URIの変更が必要
|
|
346
|
+
end note
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
rectangle "クエリパラメータ" as query {
|
|
350
|
+
note right
|
|
351
|
+
/api/orders?version=1
|
|
352
|
+
/api/orders?version=2
|
|
353
|
+
|
|
354
|
+
【メリット】
|
|
355
|
+
・URIが変わらない
|
|
356
|
+
|
|
357
|
+
【デメリット】
|
|
358
|
+
・見落としやすい
|
|
359
|
+
end note
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
rectangle "カスタムヘッダー" as header {
|
|
363
|
+
note right
|
|
364
|
+
X-API-Version: 1
|
|
365
|
+
X-API-Version: 2
|
|
366
|
+
|
|
367
|
+
【メリット】
|
|
368
|
+
・URIがクリーン
|
|
369
|
+
|
|
370
|
+
【デメリット】
|
|
371
|
+
・テストしにくい
|
|
372
|
+
end note
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
rectangle "Accept ヘッダー" as accept {
|
|
376
|
+
note right
|
|
377
|
+
Accept: application/vnd.company.v1+json
|
|
378
|
+
Accept: application/vnd.company.v2+json
|
|
379
|
+
|
|
380
|
+
【メリット】
|
|
381
|
+
・HTTP標準に準拠
|
|
382
|
+
|
|
383
|
+
【デメリット】
|
|
384
|
+
・複雑
|
|
385
|
+
end note
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@enduml
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
#### 推奨:URI パスバージョニング
|
|
392
|
+
|
|
393
|
+
```plantuml
|
|
394
|
+
@startuml
|
|
395
|
+
title URI パスバージョニングの運用
|
|
396
|
+
|
|
397
|
+
rectangle "v1(現行)" as v1 {
|
|
398
|
+
rectangle "/api/v1/orders" as v1_orders
|
|
399
|
+
rectangle "/api/v1/customers" as v1_customers
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
rectangle "v2(新版)" as v2 {
|
|
403
|
+
rectangle "/api/v2/orders" as v2_orders
|
|
404
|
+
rectangle "/api/v2/customers" as v2_customers
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
note bottom of v1
|
|
408
|
+
【v1 → v2 移行】
|
|
409
|
+
1. v2を新規リリース
|
|
410
|
+
2. 並行運用期間
|
|
411
|
+
3. v1を非推奨化(Deprecation)
|
|
412
|
+
4. v1を廃止(Sunset)
|
|
413
|
+
|
|
414
|
+
【Deprecation ヘッダー】
|
|
415
|
+
Deprecation: true
|
|
416
|
+
Sunset: Sat, 01 Jan 2025 00:00:00 GMT
|
|
417
|
+
end note
|
|
418
|
+
|
|
419
|
+
@enduml
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 38.2 サービス間通信
|
|
425
|
+
|
|
426
|
+
### 同期通信(REST / gRPC)
|
|
427
|
+
|
|
428
|
+
```plantuml
|
|
429
|
+
@startuml
|
|
430
|
+
title 同期通信パターンの比較
|
|
431
|
+
|
|
432
|
+
rectangle "REST over HTTP" as rest {
|
|
433
|
+
rectangle "販売サービス" as sales_rest
|
|
434
|
+
rectangle "在庫サービス" as inv_rest
|
|
435
|
+
|
|
436
|
+
sales_rest --> inv_rest : HTTP/JSON
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
rectangle "gRPC" as grpc {
|
|
440
|
+
rectangle "販売サービス" as sales_grpc
|
|
441
|
+
rectangle "在庫サービス" as inv_grpc
|
|
442
|
+
|
|
443
|
+
sales_grpc --> inv_grpc : HTTP/2 + Protocol Buffers
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
note bottom of rest
|
|
447
|
+
【REST】
|
|
448
|
+
・テキストベース(JSON)
|
|
449
|
+
・ブラウザから直接呼び出し可能
|
|
450
|
+
・広く普及、ツール豊富
|
|
451
|
+
|
|
452
|
+
【適用場面】
|
|
453
|
+
・外部公開API
|
|
454
|
+
・フロントエンド連携
|
|
455
|
+
end note
|
|
456
|
+
|
|
457
|
+
note bottom of grpc
|
|
458
|
+
【gRPC】
|
|
459
|
+
・バイナリプロトコル
|
|
460
|
+
・高速、低レイテンシ
|
|
461
|
+
・スキーマ定義必須
|
|
462
|
+
|
|
463
|
+
【適用場面】
|
|
464
|
+
・マイクロサービス間
|
|
465
|
+
・高性能要件
|
|
466
|
+
end note
|
|
467
|
+
|
|
468
|
+
@enduml
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
<details>
|
|
472
|
+
<summary>gRPC 定義例</summary>
|
|
473
|
+
|
|
474
|
+
```protobuf
|
|
475
|
+
syntax = "proto3";
|
|
476
|
+
|
|
477
|
+
package sales.v1;
|
|
478
|
+
|
|
479
|
+
option java_package = "com.example.sales.grpc";
|
|
480
|
+
option java_multiple_files = true;
|
|
481
|
+
|
|
482
|
+
// 在庫サービス
|
|
483
|
+
service InventoryService {
|
|
484
|
+
// 在庫照会
|
|
485
|
+
rpc GetStock(GetStockRequest) returns (StockResponse);
|
|
486
|
+
|
|
487
|
+
// 在庫引当
|
|
488
|
+
rpc AllocateStock(AllocateStockRequest) returns (AllocateStockResponse);
|
|
489
|
+
|
|
490
|
+
// 引当解除
|
|
491
|
+
rpc ReleaseStock(ReleaseStockRequest) returns (ReleaseStockResponse);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
message GetStockRequest {
|
|
495
|
+
string product_id = 1;
|
|
496
|
+
string warehouse_id = 2;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
message StockResponse {
|
|
500
|
+
string product_id = 1;
|
|
501
|
+
string warehouse_id = 2;
|
|
502
|
+
int32 quantity = 3;
|
|
503
|
+
int32 allocated_quantity = 4;
|
|
504
|
+
int32 available_quantity = 5;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
message AllocateStockRequest {
|
|
508
|
+
string product_id = 1;
|
|
509
|
+
string warehouse_id = 2;
|
|
510
|
+
int32 quantity = 3;
|
|
511
|
+
string order_id = 4;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
message AllocateStockResponse {
|
|
515
|
+
bool success = 1;
|
|
516
|
+
string allocation_id = 2;
|
|
517
|
+
string error_message = 3;
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
</details>
|
|
522
|
+
|
|
523
|
+
### 非同期通信(メッセージキュー)
|
|
524
|
+
|
|
525
|
+
```plantuml
|
|
526
|
+
@startuml
|
|
527
|
+
title 非同期通信パターン
|
|
528
|
+
|
|
529
|
+
rectangle "販売サービス" as sales
|
|
530
|
+
|
|
531
|
+
rectangle "メッセージブローカー" as broker {
|
|
532
|
+
queue "受注キュー" as order_queue
|
|
533
|
+
collections "売上トピック" as sales_topic
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
rectangle "在庫サービス" as inventory
|
|
537
|
+
rectangle "会計サービス" as accounting
|
|
538
|
+
rectangle "通知サービス" as notification
|
|
539
|
+
|
|
540
|
+
sales --> order_queue : 受注メッセージ
|
|
541
|
+
order_queue --> inventory : 在庫引当
|
|
542
|
+
|
|
543
|
+
sales --> sales_topic : 売上イベント
|
|
544
|
+
sales_topic --> accounting : 仕訳生成
|
|
545
|
+
sales_topic --> notification : 通知送信
|
|
546
|
+
sales_topic --> inventory : 在庫更新
|
|
547
|
+
|
|
548
|
+
note bottom of broker
|
|
549
|
+
【Point-to-Point】
|
|
550
|
+
・1対1の通信
|
|
551
|
+
・キューで実現
|
|
552
|
+
・負荷分散可能
|
|
553
|
+
|
|
554
|
+
【Publish-Subscribe】
|
|
555
|
+
・1対多の通信
|
|
556
|
+
・トピックで実現
|
|
557
|
+
・イベント配信
|
|
558
|
+
end note
|
|
559
|
+
|
|
560
|
+
@enduml
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
<details>
|
|
564
|
+
<summary>Java 実装例(Spring AMQP)</summary>
|
|
565
|
+
|
|
566
|
+
```java
|
|
567
|
+
// メッセージ送信
|
|
568
|
+
@Service
|
|
569
|
+
public class OrderMessagePublisher {
|
|
570
|
+
private final RabbitTemplate rabbitTemplate;
|
|
571
|
+
|
|
572
|
+
public void publishOrderCreated(OrderCreatedEvent event) {
|
|
573
|
+
rabbitTemplate.convertAndSend(
|
|
574
|
+
"sales.exchange",
|
|
575
|
+
"order.created",
|
|
576
|
+
event
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
public void publishSalesCompleted(SalesCompletedEvent event) {
|
|
581
|
+
rabbitTemplate.convertAndSend(
|
|
582
|
+
"sales.topic.exchange",
|
|
583
|
+
"sales.completed",
|
|
584
|
+
event
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// メッセージ受信
|
|
590
|
+
@Component
|
|
591
|
+
public class OrderMessageListener {
|
|
592
|
+
|
|
593
|
+
@RabbitListener(queues = "inventory.order.queue")
|
|
594
|
+
public void handleOrderCreated(OrderCreatedEvent event) {
|
|
595
|
+
// 在庫引当処理
|
|
596
|
+
log.info("Received order: {}", event.orderId());
|
|
597
|
+
inventoryService.allocate(event);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 設定
|
|
602
|
+
@Configuration
|
|
603
|
+
public class RabbitMQConfig {
|
|
604
|
+
|
|
605
|
+
@Bean
|
|
606
|
+
public TopicExchange salesTopicExchange() {
|
|
607
|
+
return new TopicExchange("sales.topic.exchange");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
@Bean
|
|
611
|
+
public Queue accountingQueue() {
|
|
612
|
+
return new Queue("accounting.sales.queue", true);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
@Bean
|
|
616
|
+
public Binding accountingBinding(Queue accountingQueue,
|
|
617
|
+
TopicExchange salesTopicExchange) {
|
|
618
|
+
return BindingBuilder.bind(accountingQueue)
|
|
619
|
+
.to(salesTopicExchange)
|
|
620
|
+
.with("sales.*");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
</details>
|
|
626
|
+
|
|
627
|
+
### サーキットブレーカーパターン
|
|
628
|
+
|
|
629
|
+
サービス間通信の障害を検知し、障害の連鎖を防ぐパターンです。
|
|
630
|
+
|
|
631
|
+
```plantuml
|
|
632
|
+
@startuml
|
|
633
|
+
title サーキットブレーカーの状態遷移
|
|
634
|
+
|
|
635
|
+
[*] --> Closed : 初期状態
|
|
636
|
+
|
|
637
|
+
Closed --> Open : 失敗が閾値を超過
|
|
638
|
+
Closed --> Closed : 成功
|
|
639
|
+
|
|
640
|
+
Open --> HalfOpen : タイムアウト後
|
|
641
|
+
Open --> Open : リクエスト拒否
|
|
642
|
+
|
|
643
|
+
HalfOpen --> Closed : 試行成功
|
|
644
|
+
HalfOpen --> Open : 試行失敗
|
|
645
|
+
|
|
646
|
+
note right of Closed
|
|
647
|
+
【Closed】
|
|
648
|
+
通常稼働
|
|
649
|
+
リクエストを通す
|
|
650
|
+
end note
|
|
651
|
+
|
|
652
|
+
note right of Open
|
|
653
|
+
【Open】
|
|
654
|
+
障害検知
|
|
655
|
+
即座にエラー返却
|
|
656
|
+
(Fail Fast)
|
|
657
|
+
end note
|
|
658
|
+
|
|
659
|
+
note right of HalfOpen
|
|
660
|
+
【Half-Open】
|
|
661
|
+
復旧確認中
|
|
662
|
+
限定的に試行
|
|
663
|
+
end note
|
|
664
|
+
|
|
665
|
+
@enduml
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
```plantuml
|
|
669
|
+
@startuml
|
|
670
|
+
title サーキットブレーカーの動作フロー
|
|
671
|
+
|
|
672
|
+
|クライアント|
|
|
673
|
+
start
|
|
674
|
+
:サービス呼び出し;
|
|
675
|
+
|
|
676
|
+
|サーキットブレーカー|
|
|
677
|
+
if (回路状態?) then (Open)
|
|
678
|
+
:即座にフォールバック;
|
|
679
|
+
|クライアント|
|
|
680
|
+
:フォールバック結果;
|
|
681
|
+
stop
|
|
682
|
+
else (Closed/HalfOpen)
|
|
683
|
+
endif
|
|
684
|
+
|
|
685
|
+
:下流サービス呼び出し;
|
|
686
|
+
|
|
687
|
+
|下流サービス|
|
|
688
|
+
if (処理成功?) then (yes)
|
|
689
|
+
:正常レスポンス;
|
|
690
|
+
|サーキットブレーカー|
|
|
691
|
+
:成功をカウント;
|
|
692
|
+
:Closedを維持/遷移;
|
|
693
|
+
else (no)
|
|
694
|
+
:エラー/タイムアウト;
|
|
695
|
+
|サーキットブレーカー|
|
|
696
|
+
:失敗をカウント;
|
|
697
|
+
|
|
698
|
+
if (失敗数 >= 閾値?) then (yes)
|
|
699
|
+
:Openに遷移;
|
|
700
|
+
endif
|
|
701
|
+
endif
|
|
702
|
+
|
|
703
|
+
|クライアント|
|
|
704
|
+
:結果を返却;
|
|
705
|
+
|
|
706
|
+
stop
|
|
707
|
+
|
|
708
|
+
@enduml
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
<details>
|
|
712
|
+
<summary>Java 実装例(Resilience4j)</summary>
|
|
713
|
+
|
|
714
|
+
```java
|
|
715
|
+
// 設定
|
|
716
|
+
@Configuration
|
|
717
|
+
public class CircuitBreakerConfig {
|
|
718
|
+
|
|
719
|
+
@Bean
|
|
720
|
+
public CircuitBreakerRegistry circuitBreakerRegistry() {
|
|
721
|
+
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
|
|
722
|
+
.failureRateThreshold(50) // 失敗率50%でOpen
|
|
723
|
+
.waitDurationInOpenState(Duration.ofSeconds(30))
|
|
724
|
+
.permittedNumberOfCallsInHalfOpenState(3)
|
|
725
|
+
.slidingWindowType(SlidingWindowType.COUNT_BASED)
|
|
726
|
+
.slidingWindowSize(10)
|
|
727
|
+
.build();
|
|
728
|
+
|
|
729
|
+
return CircuitBreakerRegistry.of(config);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// サービス
|
|
734
|
+
@Service
|
|
735
|
+
public class InventoryServiceClient {
|
|
736
|
+
private final RestTemplate restTemplate;
|
|
737
|
+
private final CircuitBreaker circuitBreaker;
|
|
738
|
+
|
|
739
|
+
public InventoryServiceClient(RestTemplate restTemplate,
|
|
740
|
+
CircuitBreakerRegistry registry) {
|
|
741
|
+
this.restTemplate = restTemplate;
|
|
742
|
+
this.circuitBreaker = registry.circuitBreaker("inventoryService");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
public StockResponse getStock(String productId) {
|
|
746
|
+
Supplier<StockResponse> supplier = CircuitBreaker
|
|
747
|
+
.decorateSupplier(circuitBreaker, () -> {
|
|
748
|
+
return restTemplate.getForObject(
|
|
749
|
+
"/api/v1/inventory/{productId}",
|
|
750
|
+
StockResponse.class,
|
|
751
|
+
productId
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
return Try.ofSupplier(supplier)
|
|
756
|
+
.recover(CallNotPermittedException.class,
|
|
757
|
+
e -> getFallbackStock(productId))
|
|
758
|
+
.recover(Exception.class,
|
|
759
|
+
e -> getFallbackStock(productId))
|
|
760
|
+
.get();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private StockResponse getFallbackStock(String productId) {
|
|
764
|
+
// フォールバック: キャッシュから取得または推定値
|
|
765
|
+
return new StockResponse(productId, 0, 0, 0, "UNKNOWN");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// アノテーションベース
|
|
770
|
+
@Service
|
|
771
|
+
public class AccountingServiceClient {
|
|
772
|
+
|
|
773
|
+
@CircuitBreaker(name = "accountingService", fallbackMethod = "fallback")
|
|
774
|
+
@Retry(name = "accountingService")
|
|
775
|
+
@TimeLimiter(name = "accountingService")
|
|
776
|
+
public CompletableFuture<JournalResponse> createJournal(
|
|
777
|
+
CreateJournalRequest request) {
|
|
778
|
+
return CompletableFuture.supplyAsync(() -> {
|
|
779
|
+
return restTemplate.postForObject(
|
|
780
|
+
"/api/v1/accounting/journals",
|
|
781
|
+
request,
|
|
782
|
+
JournalResponse.class
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
public CompletableFuture<JournalResponse> fallback(
|
|
788
|
+
CreateJournalRequest request, Throwable t) {
|
|
789
|
+
log.warn("Fallback for createJournal: {}", t.getMessage());
|
|
790
|
+
// 仕訳をキューに保存して後で再試行
|
|
791
|
+
pendingJournalQueue.add(request);
|
|
792
|
+
return CompletableFuture.completedFuture(
|
|
793
|
+
JournalResponse.pending(request.getCorrelationId())
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
</details>
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## 38.3 API ゲートウェイ
|
|
804
|
+
|
|
805
|
+
API ゲートウェイは、クライアントと複数のバックエンドサービスの間に位置し、横断的な関心事を一元的に処理します。
|
|
806
|
+
|
|
807
|
+
```plantuml
|
|
808
|
+
@startuml
|
|
809
|
+
title API ゲートウェイアーキテクチャ
|
|
810
|
+
|
|
811
|
+
rectangle "クライアント" as clients {
|
|
812
|
+
rectangle "Webアプリ" as web
|
|
813
|
+
rectangle "モバイルアプリ" as mobile
|
|
814
|
+
rectangle "外部システム" as external
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
rectangle "API Gateway" as gateway {
|
|
818
|
+
rectangle "認証・認可" as auth
|
|
819
|
+
rectangle "レート制限" as rate
|
|
820
|
+
rectangle "ルーティング" as routing
|
|
821
|
+
rectangle "ログ・監視" as logging
|
|
822
|
+
rectangle "変換・集約" as transform
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
rectangle "バックエンドサービス" as backend {
|
|
826
|
+
rectangle "販売API" as sales
|
|
827
|
+
rectangle "会計API" as accounting
|
|
828
|
+
rectangle "生産API" as production
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
web --> gateway
|
|
832
|
+
mobile --> gateway
|
|
833
|
+
external --> gateway
|
|
834
|
+
|
|
835
|
+
gateway --> sales
|
|
836
|
+
gateway --> accounting
|
|
837
|
+
gateway --> production
|
|
838
|
+
|
|
839
|
+
note bottom of gateway
|
|
840
|
+
【API Gatewayの責務】
|
|
841
|
+
・認証・認可の一元化
|
|
842
|
+
・レート制限
|
|
843
|
+
・リクエストルーティング
|
|
844
|
+
・ログ集約
|
|
845
|
+
・レスポンス変換
|
|
846
|
+
・API集約
|
|
847
|
+
end note
|
|
848
|
+
|
|
849
|
+
@enduml
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### 認証・認可の一元化
|
|
853
|
+
|
|
854
|
+
```plantuml
|
|
855
|
+
@startuml
|
|
856
|
+
title JWT 認証フロー
|
|
857
|
+
|
|
858
|
+
|クライアント|
|
|
859
|
+
start
|
|
860
|
+
:認証リクエスト;
|
|
861
|
+
note right
|
|
862
|
+
POST /auth/login
|
|
863
|
+
{username, password}
|
|
864
|
+
end note
|
|
865
|
+
|
|
866
|
+
|認証サービス|
|
|
867
|
+
:認証情報検証;
|
|
868
|
+
:JWTトークン発行;
|
|
869
|
+
note right
|
|
870
|
+
Header: {alg, typ}
|
|
871
|
+
Payload: {sub, roles, exp}
|
|
872
|
+
Signature
|
|
873
|
+
end note
|
|
874
|
+
|
|
875
|
+
|クライアント|
|
|
876
|
+
:JWTトークン保存;
|
|
877
|
+
:API呼び出し;
|
|
878
|
+
note right
|
|
879
|
+
Authorization: Bearer {token}
|
|
880
|
+
end note
|
|
881
|
+
|
|
882
|
+
|API Gateway|
|
|
883
|
+
:JWTトークン検証;
|
|
884
|
+
if (有効?) then (yes)
|
|
885
|
+
:クレーム抽出;
|
|
886
|
+
:認可チェック;
|
|
887
|
+
if (権限あり?) then (yes)
|
|
888
|
+
|バックエンドサービス|
|
|
889
|
+
:リクエスト処理;
|
|
890
|
+
:レスポンス返却;
|
|
891
|
+
else (no)
|
|
892
|
+
:403 Forbidden;
|
|
893
|
+
endif
|
|
894
|
+
else (no)
|
|
895
|
+
:401 Unauthorized;
|
|
896
|
+
endif
|
|
897
|
+
|
|
898
|
+
|クライアント|
|
|
899
|
+
:結果受信;
|
|
900
|
+
|
|
901
|
+
stop
|
|
902
|
+
|
|
903
|
+
@enduml
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
<details>
|
|
907
|
+
<summary>Java 実装例(Spring Security + JWT)</summary>
|
|
908
|
+
|
|
909
|
+
```java
|
|
910
|
+
// JWT フィルター
|
|
911
|
+
@Component
|
|
912
|
+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
913
|
+
private final JwtTokenProvider tokenProvider;
|
|
914
|
+
|
|
915
|
+
@Override
|
|
916
|
+
protected void doFilterInternal(HttpServletRequest request,
|
|
917
|
+
HttpServletResponse response,
|
|
918
|
+
FilterChain filterChain)
|
|
919
|
+
throws ServletException, IOException {
|
|
920
|
+
|
|
921
|
+
String token = extractToken(request);
|
|
922
|
+
|
|
923
|
+
if (token != null && tokenProvider.validateToken(token)) {
|
|
924
|
+
Authentication auth = tokenProvider.getAuthentication(token);
|
|
925
|
+
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
filterChain.doFilter(request, response);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private String extractToken(HttpServletRequest request) {
|
|
932
|
+
String bearerToken = request.getHeader("Authorization");
|
|
933
|
+
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
|
934
|
+
return bearerToken.substring(7);
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// JWT プロバイダー
|
|
941
|
+
@Component
|
|
942
|
+
public class JwtTokenProvider {
|
|
943
|
+
@Value("${jwt.secret}")
|
|
944
|
+
private String jwtSecret;
|
|
945
|
+
|
|
946
|
+
@Value("${jwt.expiration}")
|
|
947
|
+
private long jwtExpiration;
|
|
948
|
+
|
|
949
|
+
public String generateToken(Authentication authentication) {
|
|
950
|
+
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
|
951
|
+
|
|
952
|
+
return Jwts.builder()
|
|
953
|
+
.setSubject(userDetails.getUsername())
|
|
954
|
+
.claim("roles", userDetails.getAuthorities().stream()
|
|
955
|
+
.map(GrantedAuthority::getAuthority)
|
|
956
|
+
.collect(Collectors.toList()))
|
|
957
|
+
.setIssuedAt(new Date())
|
|
958
|
+
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
|
|
959
|
+
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
|
960
|
+
.compact();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
public boolean validateToken(String token) {
|
|
964
|
+
try {
|
|
965
|
+
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
|
|
966
|
+
return true;
|
|
967
|
+
} catch (JwtException | IllegalArgumentException e) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
public Authentication getAuthentication(String token) {
|
|
973
|
+
Claims claims = Jwts.parser()
|
|
974
|
+
.setSigningKey(jwtSecret)
|
|
975
|
+
.parseClaimsJws(token)
|
|
976
|
+
.getBody();
|
|
977
|
+
|
|
978
|
+
List<String> roles = claims.get("roles", List.class);
|
|
979
|
+
List<GrantedAuthority> authorities = roles.stream()
|
|
980
|
+
.map(SimpleGrantedAuthority::new)
|
|
981
|
+
.collect(Collectors.toList());
|
|
982
|
+
|
|
983
|
+
User principal = new User(claims.getSubject(), "", authorities);
|
|
984
|
+
return new UsernamePasswordAuthenticationToken(
|
|
985
|
+
principal, token, authorities
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
</details>
|
|
992
|
+
|
|
993
|
+
### レート制限とスロットリング
|
|
994
|
+
|
|
995
|
+
```plantuml
|
|
996
|
+
@startuml
|
|
997
|
+
title レート制限アルゴリズム
|
|
998
|
+
|
|
999
|
+
rectangle "Token Bucket" as token_bucket {
|
|
1000
|
+
note right
|
|
1001
|
+
・バケットにトークンを蓄積
|
|
1002
|
+
・リクエスト時にトークン消費
|
|
1003
|
+
・バースト対応可能
|
|
1004
|
+
|
|
1005
|
+
設定例:
|
|
1006
|
+
rate: 100/分
|
|
1007
|
+
burst: 20
|
|
1008
|
+
end note
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
rectangle "Sliding Window" as sliding_window {
|
|
1012
|
+
note right
|
|
1013
|
+
・時間枠内のリクエスト数をカウント
|
|
1014
|
+
・枠をスライドして計算
|
|
1015
|
+
・より正確な制限
|
|
1016
|
+
|
|
1017
|
+
設定例:
|
|
1018
|
+
window: 1分
|
|
1019
|
+
limit: 100
|
|
1020
|
+
end note
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
rectangle "Fixed Window" as fixed_window {
|
|
1024
|
+
note right
|
|
1025
|
+
・固定時間枠でカウント
|
|
1026
|
+
・シンプルな実装
|
|
1027
|
+
・境界でバーストの可能性
|
|
1028
|
+
|
|
1029
|
+
設定例:
|
|
1030
|
+
window: 1分
|
|
1031
|
+
limit: 100
|
|
1032
|
+
end note
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
@enduml
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
<details>
|
|
1039
|
+
<summary>Spring Cloud Gateway 設定例</summary>
|
|
1040
|
+
|
|
1041
|
+
```yaml
|
|
1042
|
+
# application.yml
|
|
1043
|
+
spring:
|
|
1044
|
+
cloud:
|
|
1045
|
+
gateway:
|
|
1046
|
+
routes:
|
|
1047
|
+
- id: sales-service
|
|
1048
|
+
uri: lb://sales-service
|
|
1049
|
+
predicates:
|
|
1050
|
+
- Path=/api/v1/sales/**
|
|
1051
|
+
filters:
|
|
1052
|
+
- name: RequestRateLimiter
|
|
1053
|
+
args:
|
|
1054
|
+
redis-rate-limiter.replenishRate: 100
|
|
1055
|
+
redis-rate-limiter.burstCapacity: 200
|
|
1056
|
+
key-resolver: "#{@userKeyResolver}"
|
|
1057
|
+
- name: CircuitBreaker
|
|
1058
|
+
args:
|
|
1059
|
+
name: salesCircuitBreaker
|
|
1060
|
+
fallbackUri: forward:/fallback/sales
|
|
1061
|
+
|
|
1062
|
+
- id: accounting-service
|
|
1063
|
+
uri: lb://accounting-service
|
|
1064
|
+
predicates:
|
|
1065
|
+
- Path=/api/v1/accounting/**
|
|
1066
|
+
filters:
|
|
1067
|
+
- name: RequestRateLimiter
|
|
1068
|
+
args:
|
|
1069
|
+
redis-rate-limiter.replenishRate: 50
|
|
1070
|
+
redis-rate-limiter.burstCapacity: 100
|
|
1071
|
+
|
|
1072
|
+
default-filters:
|
|
1073
|
+
- name: Retry
|
|
1074
|
+
args:
|
|
1075
|
+
retries: 3
|
|
1076
|
+
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
|
|
1077
|
+
methods: GET
|
|
1078
|
+
backoff:
|
|
1079
|
+
firstBackoff: 100ms
|
|
1080
|
+
maxBackoff: 500ms
|
|
1081
|
+
factor: 2
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
```java
|
|
1085
|
+
// Key Resolver(ユーザー単位のレート制限)
|
|
1086
|
+
@Configuration
|
|
1087
|
+
public class RateLimiterConfig {
|
|
1088
|
+
|
|
1089
|
+
@Bean
|
|
1090
|
+
public KeyResolver userKeyResolver() {
|
|
1091
|
+
return exchange -> {
|
|
1092
|
+
String userId = exchange.getRequest()
|
|
1093
|
+
.getHeaders()
|
|
1094
|
+
.getFirst("X-User-Id");
|
|
1095
|
+
return Mono.just(userId != null ? userId : "anonymous");
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
@Bean
|
|
1100
|
+
public KeyResolver apiKeyResolver() {
|
|
1101
|
+
return exchange -> {
|
|
1102
|
+
String apiKey = exchange.getRequest()
|
|
1103
|
+
.getHeaders()
|
|
1104
|
+
.getFirst("X-API-Key");
|
|
1105
|
+
return Mono.just(apiKey != null ? apiKey : "default");
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
</details>
|
|
1112
|
+
|
|
1113
|
+
### ログ集約とモニタリング
|
|
1114
|
+
|
|
1115
|
+
```plantuml
|
|
1116
|
+
@startuml
|
|
1117
|
+
title 分散トレーシングとログ集約
|
|
1118
|
+
|
|
1119
|
+
rectangle "クライアント" as client
|
|
1120
|
+
|
|
1121
|
+
rectangle "API Gateway" as gateway {
|
|
1122
|
+
rectangle "トレースID生成" as trace_gen
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
rectangle "サービス群" as services {
|
|
1126
|
+
rectangle "販売サービス" as sales
|
|
1127
|
+
rectangle "在庫サービス" as inventory
|
|
1128
|
+
rectangle "会計サービス" as accounting
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
rectangle "観測基盤" as observability {
|
|
1132
|
+
database "ログ集約\n(ELK/Loki)" as logs
|
|
1133
|
+
database "メトリクス\n(Prometheus)" as metrics
|
|
1134
|
+
database "トレース\n(Jaeger/Zipkin)" as traces
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
rectangle "ダッシュボード" as dashboard {
|
|
1138
|
+
rectangle "Grafana" as grafana
|
|
1139
|
+
rectangle "Kibana" as kibana
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
client --> gateway : リクエスト
|
|
1143
|
+
gateway --> sales : traceId
|
|
1144
|
+
sales --> inventory : traceId
|
|
1145
|
+
sales --> accounting : traceId
|
|
1146
|
+
|
|
1147
|
+
gateway --> logs
|
|
1148
|
+
sales --> logs
|
|
1149
|
+
inventory --> logs
|
|
1150
|
+
accounting --> logs
|
|
1151
|
+
|
|
1152
|
+
gateway --> metrics
|
|
1153
|
+
sales --> metrics
|
|
1154
|
+
inventory --> metrics
|
|
1155
|
+
accounting --> metrics
|
|
1156
|
+
|
|
1157
|
+
gateway --> traces
|
|
1158
|
+
sales --> traces
|
|
1159
|
+
inventory --> traces
|
|
1160
|
+
accounting --> traces
|
|
1161
|
+
|
|
1162
|
+
logs --> kibana
|
|
1163
|
+
metrics --> grafana
|
|
1164
|
+
traces --> grafana
|
|
1165
|
+
|
|
1166
|
+
@enduml
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
<details>
|
|
1170
|
+
<summary>Java 実装例(Micrometer + OpenTelemetry)</summary>
|
|
1171
|
+
|
|
1172
|
+
```java
|
|
1173
|
+
// トレーシング設定
|
|
1174
|
+
@Configuration
|
|
1175
|
+
public class TracingConfig {
|
|
1176
|
+
|
|
1177
|
+
@Bean
|
|
1178
|
+
public Tracer tracer() {
|
|
1179
|
+
return GlobalOpenTelemetry.getTracer("sales-service");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// カスタムメトリクス
|
|
1184
|
+
@Component
|
|
1185
|
+
public class OrderMetrics {
|
|
1186
|
+
private final MeterRegistry meterRegistry;
|
|
1187
|
+
private final Counter orderCreatedCounter;
|
|
1188
|
+
private final Timer orderProcessingTimer;
|
|
1189
|
+
|
|
1190
|
+
public OrderMetrics(MeterRegistry meterRegistry) {
|
|
1191
|
+
this.meterRegistry = meterRegistry;
|
|
1192
|
+
|
|
1193
|
+
this.orderCreatedCounter = Counter.builder("orders.created")
|
|
1194
|
+
.description("Number of orders created")
|
|
1195
|
+
.tag("service", "sales")
|
|
1196
|
+
.register(meterRegistry);
|
|
1197
|
+
|
|
1198
|
+
this.orderProcessingTimer = Timer.builder("orders.processing.time")
|
|
1199
|
+
.description("Order processing time")
|
|
1200
|
+
.tag("service", "sales")
|
|
1201
|
+
.register(meterRegistry);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
public void recordOrderCreated(String status) {
|
|
1205
|
+
orderCreatedCounter.increment();
|
|
1206
|
+
meterRegistry.counter("orders.created.by.status",
|
|
1207
|
+
"status", status).increment();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
public void recordProcessingTime(long durationMs) {
|
|
1211
|
+
orderProcessingTimer.record(Duration.ofMillis(durationMs));
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// 構造化ログ
|
|
1216
|
+
@Slf4j
|
|
1217
|
+
@Service
|
|
1218
|
+
public class OrderService {
|
|
1219
|
+
|
|
1220
|
+
public Order createOrder(CreateOrderRequest request) {
|
|
1221
|
+
MDC.put("orderId", request.getOrderId());
|
|
1222
|
+
MDC.put("customerId", request.getCustomerId());
|
|
1223
|
+
|
|
1224
|
+
try {
|
|
1225
|
+
log.info("Creating order", kv("action", "create_order_start"));
|
|
1226
|
+
|
|
1227
|
+
Order order = processOrder(request);
|
|
1228
|
+
|
|
1229
|
+
log.info("Order created successfully",
|
|
1230
|
+
kv("action", "create_order_complete"),
|
|
1231
|
+
kv("totalAmount", order.getTotalAmount()));
|
|
1232
|
+
|
|
1233
|
+
return order;
|
|
1234
|
+
} catch (Exception e) {
|
|
1235
|
+
log.error("Failed to create order",
|
|
1236
|
+
kv("action", "create_order_failed"),
|
|
1237
|
+
kv("error", e.getMessage()), e);
|
|
1238
|
+
throw e;
|
|
1239
|
+
} finally {
|
|
1240
|
+
MDC.clear();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
</details>
|
|
1247
|
+
|
|
1248
|
+
---
|
|
1249
|
+
|
|
1250
|
+
## 38.4 API インテグレーションテスト
|
|
1251
|
+
|
|
1252
|
+
### テストコンテナによる統合テスト環境
|
|
1253
|
+
|
|
1254
|
+
```plantuml
|
|
1255
|
+
@startuml
|
|
1256
|
+
title テストコンテナアーキテクチャ
|
|
1257
|
+
|
|
1258
|
+
rectangle "テスト実行環境" as test_env {
|
|
1259
|
+
rectangle "JUnit 5" as junit
|
|
1260
|
+
rectangle "テストクラス" as test_class
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
rectangle "Testcontainers" as testcontainers {
|
|
1264
|
+
rectangle "PostgreSQL\nコンテナ" as postgres
|
|
1265
|
+
rectangle "RabbitMQ\nコンテナ" as rabbitmq
|
|
1266
|
+
rectangle "Redis\nコンテナ" as redis
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
rectangle "アプリケーション" as app {
|
|
1270
|
+
rectangle "Spring Boot\nアプリケーション" as spring
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
test_class --> spring : HTTP リクエスト
|
|
1274
|
+
spring --> postgres : データアクセス
|
|
1275
|
+
spring --> rabbitmq : メッセージング
|
|
1276
|
+
spring --> redis : キャッシュ
|
|
1277
|
+
|
|
1278
|
+
junit --> testcontainers : コンテナ起動
|
|
1279
|
+
testcontainers --> postgres
|
|
1280
|
+
testcontainers --> rabbitmq
|
|
1281
|
+
testcontainers --> redis
|
|
1282
|
+
|
|
1283
|
+
note bottom of testcontainers
|
|
1284
|
+
【Testcontainersの利点】
|
|
1285
|
+
・本番同等の環境でテスト
|
|
1286
|
+
・テスト間の分離
|
|
1287
|
+
・CI/CDでの再現性
|
|
1288
|
+
・自動クリーンアップ
|
|
1289
|
+
end note
|
|
1290
|
+
|
|
1291
|
+
@enduml
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
<details>
|
|
1295
|
+
<summary>Java 実装例</summary>
|
|
1296
|
+
|
|
1297
|
+
```java
|
|
1298
|
+
// テストコンテナ設定
|
|
1299
|
+
@Testcontainers
|
|
1300
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1301
|
+
@ActiveProfiles("test")
|
|
1302
|
+
public abstract class IntegrationTestBase {
|
|
1303
|
+
|
|
1304
|
+
@Container
|
|
1305
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
|
|
1306
|
+
.withDatabaseName("testdb")
|
|
1307
|
+
.withUsername("test")
|
|
1308
|
+
.withPassword("test");
|
|
1309
|
+
|
|
1310
|
+
@Container
|
|
1311
|
+
static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.12-management");
|
|
1312
|
+
|
|
1313
|
+
@DynamicPropertySource
|
|
1314
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1315
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1316
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1317
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1318
|
+
registry.add("spring.rabbitmq.host", rabbitmq::getHost);
|
|
1319
|
+
registry.add("spring.rabbitmq.port", rabbitmq::getAmqpPort);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
@Autowired
|
|
1323
|
+
protected TestRestTemplate restTemplate;
|
|
1324
|
+
|
|
1325
|
+
@Autowired
|
|
1326
|
+
protected JdbcTemplate jdbcTemplate;
|
|
1327
|
+
|
|
1328
|
+
@BeforeEach
|
|
1329
|
+
void setUp() {
|
|
1330
|
+
// テストデータのクリーンアップ
|
|
1331
|
+
cleanupTestData();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
protected void cleanupTestData() {
|
|
1335
|
+
jdbcTemplate.execute("TRUNCATE TABLE 受注明細 CASCADE");
|
|
1336
|
+
jdbcTemplate.execute("TRUNCATE TABLE 受注 CASCADE");
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
</details>
|
|
1342
|
+
|
|
1343
|
+
### REST API エンドポイントのテスト
|
|
1344
|
+
|
|
1345
|
+
```plantuml
|
|
1346
|
+
@startuml
|
|
1347
|
+
title API テストの構造
|
|
1348
|
+
|
|
1349
|
+
' ノード間の間隔を調整
|
|
1350
|
+
skinparam nodesep 50
|
|
1351
|
+
skinparam ranksep 50
|
|
1352
|
+
|
|
1353
|
+
rectangle "== Given(前提条件)\n\n・テストデータ準備\n・認証トークン取得\n・モックの設定" as given
|
|
1354
|
+
|
|
1355
|
+
rectangle "== When(操作)\n\n・APIエンドポイント呼び出し\n・リクエストボディ設定\n・ヘッダー設定" as when
|
|
1356
|
+
|
|
1357
|
+
rectangle "== Then(検証)\n\n・ステータスコード確認\n・レスポンスボディ検証\n・データベース状態確認\n・イベント発行確認" as then
|
|
1358
|
+
|
|
1359
|
+
given --> when
|
|
1360
|
+
when --> then
|
|
1361
|
+
|
|
1362
|
+
@enduml
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
<details>
|
|
1366
|
+
<summary>Java 実装例</summary>
|
|
1367
|
+
|
|
1368
|
+
```java
|
|
1369
|
+
// 受注API統合テスト
|
|
1370
|
+
class OrderApiIntegrationTest extends IntegrationTestBase {
|
|
1371
|
+
|
|
1372
|
+
@Autowired
|
|
1373
|
+
private CustomerRepository customerRepository;
|
|
1374
|
+
|
|
1375
|
+
@Autowired
|
|
1376
|
+
private ProductRepository productRepository;
|
|
1377
|
+
|
|
1378
|
+
private String authToken;
|
|
1379
|
+
private Customer testCustomer;
|
|
1380
|
+
private Product testProduct;
|
|
1381
|
+
|
|
1382
|
+
@BeforeEach
|
|
1383
|
+
void setUpTestData() {
|
|
1384
|
+
// 認証トークン取得
|
|
1385
|
+
authToken = getAuthToken("test-user", "password");
|
|
1386
|
+
|
|
1387
|
+
// テストデータ準備
|
|
1388
|
+
testCustomer = customerRepository.save(
|
|
1389
|
+
new Customer("CUS-001", "テスト顧客", "test@example.com")
|
|
1390
|
+
);
|
|
1391
|
+
testProduct = productRepository.save(
|
|
1392
|
+
new Product("PRD-001", "テスト商品", new BigDecimal("1000"))
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
@Test
|
|
1397
|
+
@DisplayName("受注登録: 正常系")
|
|
1398
|
+
void createOrder_Success() {
|
|
1399
|
+
// Given
|
|
1400
|
+
CreateOrderRequest request = CreateOrderRequest.builder()
|
|
1401
|
+
.customerId(testCustomer.getCustomerId())
|
|
1402
|
+
.orderDate(LocalDate.now())
|
|
1403
|
+
.lines(List.of(
|
|
1404
|
+
OrderLineRequest.builder()
|
|
1405
|
+
.productId(testProduct.getProductId())
|
|
1406
|
+
.quantity(10)
|
|
1407
|
+
.build()
|
|
1408
|
+
))
|
|
1409
|
+
.build();
|
|
1410
|
+
|
|
1411
|
+
// When
|
|
1412
|
+
ResponseEntity<OrderResponse> response = restTemplate.exchange(
|
|
1413
|
+
"/api/v1/sales/orders",
|
|
1414
|
+
HttpMethod.POST,
|
|
1415
|
+
new HttpEntity<>(request, createAuthHeaders()),
|
|
1416
|
+
OrderResponse.class
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
// Then
|
|
1420
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1421
|
+
assertThat(response.getBody()).isNotNull();
|
|
1422
|
+
assertThat(response.getBody().getOrderId()).isNotBlank();
|
|
1423
|
+
assertThat(response.getBody().getStatus()).isEqualTo("DRAFT");
|
|
1424
|
+
assertThat(response.getBody().getTotalAmount())
|
|
1425
|
+
.isEqualByComparingTo(new BigDecimal("10000"));
|
|
1426
|
+
|
|
1427
|
+
// データベース状態確認
|
|
1428
|
+
Order savedOrder = orderRepository.findById(
|
|
1429
|
+
response.getBody().getOrderId()
|
|
1430
|
+
).orElseThrow();
|
|
1431
|
+
assertThat(savedOrder.getCustomerId()).isEqualTo(testCustomer.getCustomerId());
|
|
1432
|
+
assertThat(savedOrder.getLines()).hasSize(1);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
@Test
|
|
1436
|
+
@DisplayName("受注登録: バリデーションエラー")
|
|
1437
|
+
void createOrder_ValidationError() {
|
|
1438
|
+
// Given - 明細なしのリクエスト
|
|
1439
|
+
CreateOrderRequest request = CreateOrderRequest.builder()
|
|
1440
|
+
.customerId(testCustomer.getCustomerId())
|
|
1441
|
+
.orderDate(LocalDate.now())
|
|
1442
|
+
.lines(List.of()) // 空の明細
|
|
1443
|
+
.build();
|
|
1444
|
+
|
|
1445
|
+
// When
|
|
1446
|
+
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1447
|
+
"/api/v1/sales/orders",
|
|
1448
|
+
HttpMethod.POST,
|
|
1449
|
+
new HttpEntity<>(request, createAuthHeaders()),
|
|
1450
|
+
ProblemDetail.class
|
|
1451
|
+
);
|
|
1452
|
+
|
|
1453
|
+
// Then
|
|
1454
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
|
1455
|
+
assertThat(response.getBody().getTitle()).isEqualTo("Validation Error");
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
@Test
|
|
1459
|
+
@DisplayName("受注一覧取得: ページング")
|
|
1460
|
+
void getOrders_Paging() {
|
|
1461
|
+
// Given - 複数の受注を作成
|
|
1462
|
+
for (int i = 0; i < 25; i++) {
|
|
1463
|
+
createTestOrder("ORD-" + String.format("%03d", i));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// When
|
|
1467
|
+
ResponseEntity<PagedResponse<OrderSummary>> response = restTemplate.exchange(
|
|
1468
|
+
"/api/v1/sales/orders?page=0&size=10",
|
|
1469
|
+
HttpMethod.GET,
|
|
1470
|
+
new HttpEntity<>(createAuthHeaders()),
|
|
1471
|
+
new ParameterizedTypeReference<>() {}
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
// Then
|
|
1475
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1476
|
+
assertThat(response.getBody().getContent()).hasSize(10);
|
|
1477
|
+
assertThat(response.getBody().getTotalElements()).isEqualTo(25);
|
|
1478
|
+
assertThat(response.getBody().getTotalPages()).isEqualTo(3);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
private HttpHeaders createAuthHeaders() {
|
|
1482
|
+
HttpHeaders headers = new HttpHeaders();
|
|
1483
|
+
headers.setBearerAuth(authToken);
|
|
1484
|
+
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
1485
|
+
return headers;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
</details>
|
|
1491
|
+
|
|
1492
|
+
### サービス間連携テスト
|
|
1493
|
+
|
|
1494
|
+
```plantuml
|
|
1495
|
+
@startuml
|
|
1496
|
+
title サービス間連携テストのアプローチ
|
|
1497
|
+
|
|
1498
|
+
' ボックスの定義
|
|
1499
|
+
rectangle "Contract Testing" as contract
|
|
1500
|
+
note right of contract
|
|
1501
|
+
・Pact / Spring Cloud Contract
|
|
1502
|
+
・プロバイダーとコンシューマーの契約
|
|
1503
|
+
・独立したテスト実行
|
|
1504
|
+
end note
|
|
1505
|
+
|
|
1506
|
+
rectangle "Component Testing" as component
|
|
1507
|
+
note right of component
|
|
1508
|
+
・WireMockで外部サービスをモック
|
|
1509
|
+
・単一サービスの統合テスト
|
|
1510
|
+
・高速なフィードバック
|
|
1511
|
+
end note
|
|
1512
|
+
|
|
1513
|
+
rectangle "End-to-End Testing" as e2e
|
|
1514
|
+
note right of e2e
|
|
1515
|
+
・全サービスを起動
|
|
1516
|
+
・Docker Composeで環境構築
|
|
1517
|
+
・本番同等のシナリオテスト
|
|
1518
|
+
end note
|
|
1519
|
+
|
|
1520
|
+
' 配置(上から下に並べる)
|
|
1521
|
+
contract -[hidden]down-> component
|
|
1522
|
+
component -[hidden]down-> e2e
|
|
1523
|
+
|
|
1524
|
+
@enduml
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
<details>
|
|
1528
|
+
<summary>Java 実装例(WireMock)</summary>
|
|
1529
|
+
|
|
1530
|
+
```java
|
|
1531
|
+
// WireMockを使用したサービス間連携テスト
|
|
1532
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1533
|
+
@AutoConfigureWireMock(port = 0)
|
|
1534
|
+
class OrderServiceWithInventoryTest {
|
|
1535
|
+
|
|
1536
|
+
@Autowired
|
|
1537
|
+
private TestRestTemplate restTemplate;
|
|
1538
|
+
|
|
1539
|
+
@Value("${wiremock.server.port}")
|
|
1540
|
+
private int wireMockPort;
|
|
1541
|
+
|
|
1542
|
+
@BeforeEach
|
|
1543
|
+
void setUp() {
|
|
1544
|
+
// 在庫サービスのモック設定
|
|
1545
|
+
stubFor(get(urlPathMatching("/api/v1/inventory/.*"))
|
|
1546
|
+
.willReturn(aResponse()
|
|
1547
|
+
.withStatus(200)
|
|
1548
|
+
.withHeader("Content-Type", "application/json")
|
|
1549
|
+
.withBody("""
|
|
1550
|
+
{
|
|
1551
|
+
"productId": "PRD-001",
|
|
1552
|
+
"quantity": 100,
|
|
1553
|
+
"allocatedQuantity": 0,
|
|
1554
|
+
"availableQuantity": 100
|
|
1555
|
+
}
|
|
1556
|
+
""")));
|
|
1557
|
+
|
|
1558
|
+
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1559
|
+
.willReturn(aResponse()
|
|
1560
|
+
.withStatus(200)
|
|
1561
|
+
.withHeader("Content-Type", "application/json")
|
|
1562
|
+
.withBody("""
|
|
1563
|
+
{
|
|
1564
|
+
"success": true,
|
|
1565
|
+
"allocationId": "ALLOC-001"
|
|
1566
|
+
}
|
|
1567
|
+
""")));
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
@Test
|
|
1571
|
+
@DisplayName("受注確定: 在庫引当成功")
|
|
1572
|
+
void confirmOrder_InventoryAllocated() {
|
|
1573
|
+
// Given
|
|
1574
|
+
String orderId = createDraftOrder();
|
|
1575
|
+
|
|
1576
|
+
// When
|
|
1577
|
+
ResponseEntity<OrderResponse> response = restTemplate.exchange(
|
|
1578
|
+
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1579
|
+
HttpMethod.POST,
|
|
1580
|
+
new HttpEntity<>(createAuthHeaders()),
|
|
1581
|
+
OrderResponse.class,
|
|
1582
|
+
orderId
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
// Then
|
|
1586
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1587
|
+
assertThat(response.getBody().getStatus()).isEqualTo("CONFIRMED");
|
|
1588
|
+
|
|
1589
|
+
// 在庫サービスへの呼び出しを検証
|
|
1590
|
+
verify(postRequestedFor(urlPathEqualTo("/api/v1/inventory/allocate"))
|
|
1591
|
+
.withRequestBody(matchingJsonPath("$.productId", equalTo("PRD-001")))
|
|
1592
|
+
.withRequestBody(matchingJsonPath("$.quantity", equalTo("10"))));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
@Test
|
|
1596
|
+
@DisplayName("受注確定: 在庫不足")
|
|
1597
|
+
void confirmOrder_InsufficientStock() {
|
|
1598
|
+
// Given - 在庫不足のレスポンスを設定
|
|
1599
|
+
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1600
|
+
.willReturn(aResponse()
|
|
1601
|
+
.withStatus(200)
|
|
1602
|
+
.withBody("""
|
|
1603
|
+
{
|
|
1604
|
+
"success": false,
|
|
1605
|
+
"errorMessage": "Insufficient stock"
|
|
1606
|
+
}
|
|
1607
|
+
""")));
|
|
1608
|
+
|
|
1609
|
+
String orderId = createDraftOrder();
|
|
1610
|
+
|
|
1611
|
+
// When
|
|
1612
|
+
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1613
|
+
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1614
|
+
HttpMethod.POST,
|
|
1615
|
+
new HttpEntity<>(createAuthHeaders()),
|
|
1616
|
+
ProblemDetail.class,
|
|
1617
|
+
orderId
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
// Then
|
|
1621
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
|
|
1622
|
+
assertThat(response.getBody().getDetail()).contains("在庫不足");
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
@Test
|
|
1626
|
+
@DisplayName("受注確定: 在庫サービスタイムアウト")
|
|
1627
|
+
void confirmOrder_InventoryServiceTimeout() {
|
|
1628
|
+
// Given - タイムアウトをシミュレート
|
|
1629
|
+
stubFor(post(urlPathMatching("/api/v1/inventory/allocate"))
|
|
1630
|
+
.willReturn(aResponse()
|
|
1631
|
+
.withFixedDelay(5000) // 5秒遅延
|
|
1632
|
+
.withStatus(200)));
|
|
1633
|
+
|
|
1634
|
+
String orderId = createDraftOrder();
|
|
1635
|
+
|
|
1636
|
+
// When
|
|
1637
|
+
ResponseEntity<ProblemDetail> response = restTemplate.exchange(
|
|
1638
|
+
"/api/v1/sales/orders/{orderId}/confirm",
|
|
1639
|
+
HttpMethod.POST,
|
|
1640
|
+
new HttpEntity<>(createAuthHeaders()),
|
|
1641
|
+
ProblemDetail.class,
|
|
1642
|
+
orderId
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// Then - サーキットブレーカーによるフォールバック
|
|
1646
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
</details>
|
|
1652
|
+
|
|
1653
|
+
---
|
|
1654
|
+
|
|
1655
|
+
## 38.5 まとめ
|
|
1656
|
+
|
|
1657
|
+
本章では、API 設計とサービス連携のパターンについて解説しました。
|
|
1658
|
+
|
|
1659
|
+
### 学んだこと
|
|
1660
|
+
|
|
1661
|
+
1. **API 設計の原則**
|
|
1662
|
+
|
|
1663
|
+
- RESTful API の制約と設計原則
|
|
1664
|
+
- リソース指向設計(名詞ベース、HTTP メソッド)
|
|
1665
|
+
- バージョニング戦略(URI パス推奨)
|
|
1666
|
+
|
|
1667
|
+
2. **サービス間通信**
|
|
1668
|
+
|
|
1669
|
+
- 同期通信(REST、gRPC)の使い分け
|
|
1670
|
+
- 非同期通信(メッセージキュー)
|
|
1671
|
+
- サーキットブレーカーによる障害対策
|
|
1672
|
+
|
|
1673
|
+
3. **API ゲートウェイ**
|
|
1674
|
+
|
|
1675
|
+
- 認証・認可の一元化(JWT)
|
|
1676
|
+
- レート制限とスロットリング
|
|
1677
|
+
- ログ集約とモニタリング
|
|
1678
|
+
|
|
1679
|
+
4. **API インテグレーションテスト**
|
|
1680
|
+
|
|
1681
|
+
- テストコンテナによる統合テスト環境
|
|
1682
|
+
- REST API エンドポイントのテスト
|
|
1683
|
+
- サービス間連携テスト(WireMock)
|
|
1684
|
+
|
|
1685
|
+
### API 設計チェックリスト
|
|
1686
|
+
|
|
1687
|
+
- [ ] リソースは名詞(複数形)で命名されているか
|
|
1688
|
+
- [ ] HTTP メソッドが正しく使用されているか
|
|
1689
|
+
- [ ] エラーレスポンスは Problem Detail 形式か
|
|
1690
|
+
- [ ] バージョニング戦略が決定されているか
|
|
1691
|
+
- [ ] 認証・認可が適切に実装されているか
|
|
1692
|
+
- [ ] レート制限が設定されているか
|
|
1693
|
+
- [ ] サーキットブレーカーが導入されているか
|
|
1694
|
+
- [ ] 統合テストが整備されているか
|
|
1695
|
+
|
|
1696
|
+
### 次章の予告
|
|
1697
|
+
|
|
1698
|
+
第39章では、データ連携の実装パターンについて解説します。バッチ連携、リアルタイム連携、連携テーブルの設計など、具体的な実装パターンを学びます。
|