[{"data":1,"prerenderedAt":8202},["ShallowReactive",2],{"post-git-worktrees-parallel-development":3,"rel-git-worktrees-parallel-development":1243,"sib-git-worktrees-parallel-development":8107},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"date":11,"subtitle":12,"image":13,"optimized_image":13,"category":14,"tags":15,"author":19,"paginate":6,"body":20,"_type":1237,"_id":1238,"_source":1239,"_file":1240,"_stem":1241,"_extension":1242},"/posts/git-worktrees-parallel-development","posts",false,"","Git Worktrees: Parallel Work Without Clobbering Your Workspace","A hands-on guide to Git worktrees: create isolated checkouts, run parallel development and AI agents, manage dependencies and ports, integrate safely, and clean up.","post","2025-07-15T11:00:00.000Z","A practical workflow for separate tasks, builds, and AI coding agents in one repository","/assets/img/uploads/git-worktrees-parallel-development.jpg","code",[14,16,17,18],"git","ai","best practices","jaimedearcos",{"type":21,"children":22,"toc":1227},"root",[23,31,44,49,56,104,169,190,195,201,220,336,387,408,413,464,484,490,495,515,648,653,666,672,677,716,721,726,731,737,763,841,867,872,935,968,994,1000,1005,1047,1098,1133,1139,1144,1174,1179,1185,1221],{"type":24,"tag":25,"props":26,"children":27},"element","p",{},[28],{"type":29,"value":30},"text","You are halfway through a search refactor when an urgent bug arrives. Or an AI coding agent is editing the API while you want another agent to work on tests. One checkout forces you to stop, stash, switch branches, and later reconstruct the context you had. Two agents in that checkout can overwrite each other's files or mistake unfinished work for their own.",{"type":24,"tag":25,"props":32,"children":33},{},[34,36,42],{"type":29,"value":35},"Git worktrees give each task a separate working directory and branch while keeping them attached to the same repository. This is useful for human parallelism, and especially useful now that several AI agents can produce useful code at the same time. ",{"type":24,"tag":37,"props":38,"children":39},"strong",{},[40],{"type":29,"value":41},"Parallel coding only helps when each task has an explicit boundary and the results can be integrated safely.",{"type":29,"value":43}," Worktrees provide the filesystem boundary; they do not make design decisions or resolve conflicting changes for you.",{"type":24,"tag":25,"props":45,"children":46},{},[47],{"type":29,"value":48},"Here is a workflow you can try with a repository you already have.",{"type":24,"tag":50,"props":51,"children":53},"h2",{"id":52},"what-a-worktree-actually-separates",[54],{"type":29,"value":55},"What a worktree actually separates",{"type":24,"tag":25,"props":57,"children":58},{},[59,61,67,69,75,77,83,85,91,93,102],{"type":29,"value":60},"A linked worktree has its own checked-out files, ",{"type":24,"tag":14,"props":62,"children":64},{"className":63},[],[65],{"type":29,"value":66},"HEAD",{"type":29,"value":68},", index, and uncommitted changes. The worktrees share the repository's objects and branch references. You can have ",{"type":24,"tag":14,"props":70,"children":72},{"className":71},[],[73],{"type":29,"value":74},"main",{"type":29,"value":76}," open in one directory, ",{"type":24,"tag":14,"props":78,"children":80},{"className":79},[],[81],{"type":29,"value":82},"feature/search-index",{"type":29,"value":84}," in another, and ",{"type":24,"tag":14,"props":86,"children":88},{"className":87},[],[89],{"type":29,"value":90},"feature/api-tests",{"type":29,"value":92}," in a third without cloning the repository three times. Git documents this model in ",{"type":24,"tag":94,"props":95,"children":99},"a",{"href":96,"rel":97},"https://git-scm.com/docs/git-worktree",[98],"nofollow",[100],{"type":29,"value":101},"git-worktree",{"type":29,"value":103},".",{"type":24,"tag":105,"props":106,"children":109},"pre",{"className":107,"code":108,"language":29,"meta":7,"style":7},"language-text shiki shiki-themes github-dark github-light","workspace/\n├── project/                         main; clean integration checkout\n└── .worktrees/\n    └── project/\n        ├── search-index/            feature/search-index\n        └── api-tests/               feature/api-tests\n",[110],{"type":24,"tag":14,"props":111,"children":112},{"__ignoreMap":7},[113,124,133,142,151,160],{"type":24,"tag":114,"props":115,"children":118},"span",{"class":116,"line":117},"line",1,[119],{"type":24,"tag":114,"props":120,"children":121},{},[122],{"type":29,"value":123},"workspace/\n",{"type":24,"tag":114,"props":125,"children":127},{"class":116,"line":126},2,[128],{"type":24,"tag":114,"props":129,"children":130},{},[131],{"type":29,"value":132},"├── project/                         main; clean integration checkout\n",{"type":24,"tag":114,"props":134,"children":136},{"class":116,"line":135},3,[137],{"type":24,"tag":114,"props":138,"children":139},{},[140],{"type":29,"value":141},"└── .worktrees/\n",{"type":24,"tag":114,"props":143,"children":145},{"class":116,"line":144},4,[146],{"type":24,"tag":114,"props":147,"children":148},{},[149],{"type":29,"value":150},"    └── project/\n",{"type":24,"tag":114,"props":152,"children":154},{"class":116,"line":153},5,[155],{"type":24,"tag":114,"props":156,"children":157},{},[158],{"type":29,"value":159},"        ├── search-index/            feature/search-index\n",{"type":24,"tag":114,"props":161,"children":163},{"class":116,"line":162},6,[164],{"type":24,"tag":114,"props":165,"children":166},{},[167],{"type":29,"value":168},"        └── api-tests/               feature/api-tests\n",{"type":24,"tag":25,"props":170,"children":171},{},[172,174,180,182,188],{"type":29,"value":173},"This layout keeps task checkouts outside the main checkout, so a file search or build in ",{"type":24,"tag":14,"props":175,"children":177},{"className":176},[],[178],{"type":29,"value":179},"project/",{"type":29,"value":181}," does not accidentally traverse the other tasks. Choose a location your tools can discover and that your team can inspect. ",{"type":24,"tag":14,"props":183,"children":185},{"className":184},[],[186],{"type":29,"value":187},"git worktree list",{"type":29,"value":189}," shows every worktree Git knows about, regardless of where you put it.",{"type":24,"tag":25,"props":191,"children":192},{},[193],{"type":29,"value":194},"Git normally prevents the same branch from being checked out in two worktrees at once. That safeguard matters: each task needs its own branch. Starting two tasks from the same commit is fine; letting them edit the same branch or directory is the collision we are trying to avoid.",{"type":24,"tag":50,"props":196,"children":198},{"id":197},"create-two-task-checkouts",[199],{"type":29,"value":200},"Create two task checkouts",{"type":24,"tag":25,"props":202,"children":203},{},[204,206,211,213,218],{"type":29,"value":205},"Run these commands from the existing ",{"type":24,"tag":14,"props":207,"children":209},{"className":208},[],[210],{"type":29,"value":179},{"type":29,"value":212}," checkout while ",{"type":24,"tag":14,"props":214,"children":216},{"className":215},[],[217],{"type":29,"value":74},{"type":29,"value":219}," is clean:",{"type":24,"tag":105,"props":221,"children":225},{"className":222,"code":223,"language":224,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark github-light","mkdir -p ../.worktrees/project\ngit worktree add -b feature/search-index ../.worktrees/project/search-index main\ngit worktree add -b feature/api-tests ../.worktrees/project/api-tests main\ngit worktree list\n","bash",[226],{"type":24,"tag":14,"props":227,"children":228},{"__ignoreMap":7},[229,250,287,320],{"type":24,"tag":114,"props":230,"children":231},{"class":116,"line":117},[232,238,244],{"type":24,"tag":114,"props":233,"children":235},{"style":234},"--shiki-default:#B392F0;--shiki-light:#6F42C1",[236],{"type":29,"value":237},"mkdir",{"type":24,"tag":114,"props":239,"children":241},{"style":240},"--shiki-default:#79B8FF;--shiki-light:#005CC5",[242],{"type":29,"value":243}," -p",{"type":24,"tag":114,"props":245,"children":247},{"style":246},"--shiki-default:#9ECBFF;--shiki-light:#032F62",[248],{"type":29,"value":249}," ../.worktrees/project\n",{"type":24,"tag":114,"props":251,"children":252},{"class":116,"line":126},[253,257,262,267,272,277,282],{"type":24,"tag":114,"props":254,"children":255},{"style":234},[256],{"type":29,"value":16},{"type":24,"tag":114,"props":258,"children":259},{"style":246},[260],{"type":29,"value":261}," worktree",{"type":24,"tag":114,"props":263,"children":264},{"style":246},[265],{"type":29,"value":266}," add",{"type":24,"tag":114,"props":268,"children":269},{"style":240},[270],{"type":29,"value":271}," -b",{"type":24,"tag":114,"props":273,"children":274},{"style":246},[275],{"type":29,"value":276}," feature/search-index",{"type":24,"tag":114,"props":278,"children":279},{"style":246},[280],{"type":29,"value":281}," ../.worktrees/project/search-index",{"type":24,"tag":114,"props":283,"children":284},{"style":246},[285],{"type":29,"value":286}," main\n",{"type":24,"tag":114,"props":288,"children":289},{"class":116,"line":135},[290,294,298,302,306,311,316],{"type":24,"tag":114,"props":291,"children":292},{"style":234},[293],{"type":29,"value":16},{"type":24,"tag":114,"props":295,"children":296},{"style":246},[297],{"type":29,"value":261},{"type":24,"tag":114,"props":299,"children":300},{"style":246},[301],{"type":29,"value":266},{"type":24,"tag":114,"props":303,"children":304},{"style":240},[305],{"type":29,"value":271},{"type":24,"tag":114,"props":307,"children":308},{"style":246},[309],{"type":29,"value":310}," feature/api-tests",{"type":24,"tag":114,"props":312,"children":313},{"style":246},[314],{"type":29,"value":315}," ../.worktrees/project/api-tests",{"type":24,"tag":114,"props":317,"children":318},{"style":246},[319],{"type":29,"value":286},{"type":24,"tag":114,"props":321,"children":322},{"class":116,"line":144},[323,327,331],{"type":24,"tag":114,"props":324,"children":325},{"style":234},[326],{"type":29,"value":16},{"type":24,"tag":114,"props":328,"children":329},{"style":246},[330],{"type":29,"value":261},{"type":24,"tag":114,"props":332,"children":333},{"style":246},[334],{"type":29,"value":335}," list\n",{"type":24,"tag":25,"props":337,"children":338},{},[339,341,347,349,354,356,361,363,370,372,378,380,386],{"type":29,"value":340},"The ",{"type":24,"tag":14,"props":342,"children":344},{"className":343},[],[345],{"type":29,"value":346},"-b",{"type":29,"value":348}," flag creates each branch at ",{"type":24,"tag":14,"props":350,"children":352},{"className":351},[],[353],{"type":29,"value":74},{"type":29,"value":355}," and checks it out in the new directory. The final ",{"type":24,"tag":14,"props":357,"children":359},{"className":358},[],[360],{"type":29,"value":74},{"type":29,"value":362}," is the starting commit, stated explicitly so the result does not depend on whichever branch happens to be active in your shell. The ",{"type":24,"tag":94,"props":364,"children":367},{"href":365,"rel":366},"https://git-scm.com/docs/git-worktree#_commands",[98],[368],{"type":29,"value":369},"Git manual",{"type":29,"value":371}," describes both ",{"type":24,"tag":14,"props":373,"children":375},{"className":374},[],[376],{"type":29,"value":377},"add",{"type":29,"value":379}," and ",{"type":24,"tag":14,"props":381,"children":383},{"className":382},[],[384],{"type":29,"value":385},"list",{"type":29,"value":103},{"type":24,"tag":25,"props":388,"children":389},{},[390,392,398,400,406],{"type":29,"value":391},"Give each task a short, testable objective. For example: “Add an index for the order search query and measure the plan before and after” in ",{"type":24,"tag":14,"props":393,"children":395},{"className":394},[],[396],{"type":29,"value":397},"search-index",{"type":29,"value":399},"; “Add API contract tests for the current order response” in ",{"type":24,"tag":14,"props":401,"children":403},{"className":402},[],[404],{"type":29,"value":405},"api-tests",{"type":29,"value":407},". If both tasks need to rewrite the same handler, separate worktrees will prevent filesystem collisions but the integration may still be difficult. Split the work by stable interfaces, or sequence the dependent edits.",{"type":24,"tag":25,"props":409,"children":410},{},[411],{"type":29,"value":412},"Move into one directory to work on it:",{"type":24,"tag":105,"props":414,"children":416},{"className":222,"code":415,"language":224,"meta":7,"style":7},"cd ../.worktrees/project/search-index\ngit status --short --branch\n# Edit, run checks, and commit on feature/search-index.\n",[417],{"type":24,"tag":14,"props":418,"children":419},{"__ignoreMap":7},[420,433,455],{"type":24,"tag":114,"props":421,"children":422},{"class":116,"line":117},[423,428],{"type":24,"tag":114,"props":424,"children":425},{"style":240},[426],{"type":29,"value":427},"cd",{"type":24,"tag":114,"props":429,"children":430},{"style":246},[431],{"type":29,"value":432}," ../.worktrees/project/search-index\n",{"type":24,"tag":114,"props":434,"children":435},{"class":116,"line":126},[436,440,445,450],{"type":24,"tag":114,"props":437,"children":438},{"style":234},[439],{"type":29,"value":16},{"type":24,"tag":114,"props":441,"children":442},{"style":246},[443],{"type":29,"value":444}," status",{"type":24,"tag":114,"props":446,"children":447},{"style":240},[448],{"type":29,"value":449}," --short",{"type":24,"tag":114,"props":451,"children":452},{"style":240},[453],{"type":29,"value":454}," --branch\n",{"type":24,"tag":114,"props":456,"children":457},{"class":116,"line":135},[458],{"type":24,"tag":114,"props":459,"children":461},{"style":460},"--shiki-default:#6A737D;--shiki-light:#6A737D",[462],{"type":29,"value":463},"# Edit, run checks, and commit on feature/search-index.\n",{"type":24,"tag":25,"props":465,"children":466},{},[467,469,475,477,482],{"type":29,"value":468},"From that directory, ",{"type":24,"tag":14,"props":470,"children":472},{"className":471},[],[473],{"type":29,"value":474},"git status",{"type":29,"value":476}," describes only that task's checkout. Changes there will not appear in the ",{"type":24,"tag":14,"props":478,"children":480},{"className":479},[],[481],{"type":29,"value":405},{"type":29,"value":483}," working directory. New commits and branches do become visible through the shared repository, which is why integration still needs coordination.",{"type":24,"tag":50,"props":485,"children":487},{"id":486},"give-each-checkout-its-own-runtime",[488],{"type":29,"value":489},"Give each checkout its own runtime",{"type":24,"tag":25,"props":491,"children":492},{},[493],{"type":29,"value":494},"Worktrees separate tracked files and untracked files on disk. They do not automatically isolate everything your application uses. A local database, a Docker project name, an output bucket, and TCP port 3000 can still be shared accidentally.",{"type":24,"tag":25,"props":496,"children":497},{},[498,500,506,508,513],{"type":29,"value":499},"For a Node project, install dependencies in each worktree. Do not symlink one ",{"type":24,"tag":14,"props":501,"children":503},{"className":502},[],[504],{"type":29,"value":505},"node_modules",{"type":29,"value":507}," directory into several worktrees: package tooling and build caches can resolve paths relative to the checkout. Use the project's lockfile and version manager, then start each server on a distinct port. Start each terminal in ",{"type":24,"tag":14,"props":509,"children":511},{"className":510},[],[512],{"type":29,"value":179},{"type":29,"value":514}," for this example:",{"type":24,"tag":105,"props":516,"children":518},{"className":222,"code":517,"language":224,"meta":7,"style":7},"cd ../.worktrees/project/search-index\nnpm ci\nnpm run dev -- --port 3101\n\n# In a second terminal:\ncd ../.worktrees/project/api-tests\nnpm ci\nnpm run dev -- --port 3102\n",[519],{"type":24,"tag":14,"props":520,"children":521},{"__ignoreMap":7},[522,533,546,578,587,595,607,619],{"type":24,"tag":114,"props":523,"children":524},{"class":116,"line":117},[525,529],{"type":24,"tag":114,"props":526,"children":527},{"style":240},[528],{"type":29,"value":427},{"type":24,"tag":114,"props":530,"children":531},{"style":246},[532],{"type":29,"value":432},{"type":24,"tag":114,"props":534,"children":535},{"class":116,"line":126},[536,541],{"type":24,"tag":114,"props":537,"children":538},{"style":234},[539],{"type":29,"value":540},"npm",{"type":24,"tag":114,"props":542,"children":543},{"style":246},[544],{"type":29,"value":545}," ci\n",{"type":24,"tag":114,"props":547,"children":548},{"class":116,"line":135},[549,553,558,563,568,573],{"type":24,"tag":114,"props":550,"children":551},{"style":234},[552],{"type":29,"value":540},{"type":24,"tag":114,"props":554,"children":555},{"style":246},[556],{"type":29,"value":557}," run",{"type":24,"tag":114,"props":559,"children":560},{"style":246},[561],{"type":29,"value":562}," dev",{"type":24,"tag":114,"props":564,"children":565},{"style":240},[566],{"type":29,"value":567}," --",{"type":24,"tag":114,"props":569,"children":570},{"style":240},[571],{"type":29,"value":572}," --port",{"type":24,"tag":114,"props":574,"children":575},{"style":240},[576],{"type":29,"value":577}," 3101\n",{"type":24,"tag":114,"props":579,"children":580},{"class":116,"line":144},[581],{"type":24,"tag":114,"props":582,"children":584},{"emptyLinePlaceholder":583},true,[585],{"type":29,"value":586},"\n",{"type":24,"tag":114,"props":588,"children":589},{"class":116,"line":153},[590],{"type":24,"tag":114,"props":591,"children":592},{"style":460},[593],{"type":29,"value":594},"# In a second terminal:\n",{"type":24,"tag":114,"props":596,"children":597},{"class":116,"line":162},[598,602],{"type":24,"tag":114,"props":599,"children":600},{"style":240},[601],{"type":29,"value":427},{"type":24,"tag":114,"props":603,"children":604},{"style":246},[605],{"type":29,"value":606}," ../.worktrees/project/api-tests\n",{"type":24,"tag":114,"props":608,"children":610},{"class":116,"line":609},7,[611,615],{"type":24,"tag":114,"props":612,"children":613},{"style":234},[614],{"type":29,"value":540},{"type":24,"tag":114,"props":616,"children":617},{"style":246},[618],{"type":29,"value":545},{"type":24,"tag":114,"props":620,"children":622},{"class":116,"line":621},8,[623,627,631,635,639,643],{"type":24,"tag":114,"props":624,"children":625},{"style":234},[626],{"type":29,"value":540},{"type":24,"tag":114,"props":628,"children":629},{"style":246},[630],{"type":29,"value":557},{"type":24,"tag":114,"props":632,"children":633},{"style":246},[634],{"type":29,"value":562},{"type":24,"tag":114,"props":636,"children":637},{"style":240},[638],{"type":29,"value":567},{"type":24,"tag":114,"props":640,"children":641},{"style":240},[642],{"type":29,"value":572},{"type":24,"tag":114,"props":644,"children":645},{"style":240},[646],{"type":29,"value":647}," 3102\n",{"type":24,"tag":25,"props":649,"children":650},{},[651],{"type":29,"value":652},"The commands are illustrative; use your repository's build and start commands. Keep task-specific environment variables and local data separate where those resources can interfere. Never copy production secrets into a worktree merely to make a local preview start.",{"type":24,"tag":25,"props":654,"children":655},{},[656,658,664],{"type":29,"value":657},"Ignored files are also local to a worktree. A ",{"type":24,"tag":14,"props":659,"children":661},{"className":660},[],[662],{"type":29,"value":663},".env.local",{"type":29,"value":665}," file or generated build in one directory does not appear in another. This is convenient, but it means a fresh worktree may need its own safe local configuration before it can run.",{"type":24,"tag":50,"props":667,"children":669},{"id":668},"one-agent-one-worktree-one-objective",[670],{"type":29,"value":671},"One agent, one worktree, one objective",{"type":24,"tag":25,"props":673,"children":674},{},[675],{"type":29,"value":676},"An AI agent should be given the absolute path of its worktree and a bounded task. That makes its file operations, terminal commands, and build artifacts easy to attribute. A useful brief looks like this:",{"type":24,"tag":105,"props":678,"children":680},{"className":107,"code":679,"language":29,"meta":7,"style":7},"Work only in /workspace/.worktrees/project/search-index.\nGoal: improve the order search query without changing the API response.\nRun the repository's relevant tests and report the query plan change.\nCommit your work on feature/search-index; do not integrate or deploy it.\n",[681],{"type":24,"tag":14,"props":682,"children":683},{"__ignoreMap":7},[684,692,700,708],{"type":24,"tag":114,"props":685,"children":686},{"class":116,"line":117},[687],{"type":24,"tag":114,"props":688,"children":689},{},[690],{"type":29,"value":691},"Work only in /workspace/.worktrees/project/search-index.\n",{"type":24,"tag":114,"props":693,"children":694},{"class":116,"line":126},[695],{"type":24,"tag":114,"props":696,"children":697},{},[698],{"type":29,"value":699},"Goal: improve the order search query without changing the API response.\n",{"type":24,"tag":114,"props":701,"children":702},{"class":116,"line":135},[703],{"type":24,"tag":114,"props":704,"children":705},{},[706],{"type":29,"value":707},"Run the repository's relevant tests and report the query plan change.\n",{"type":24,"tag":114,"props":709,"children":710},{"class":116,"line":144},[711],{"type":24,"tag":114,"props":712,"children":713},{},[714],{"type":29,"value":715},"Commit your work on feature/search-index; do not integrate or deploy it.\n",{"type":24,"tag":25,"props":717,"children":718},{},[719],{"type":29,"value":720},"Give the second agent a different directory and objective. Do not send two agents into the same worktree, even if their prompts mention different files. A formatter, generated schema, package update, or broad search-and-replace can cross that informal boundary.",{"type":24,"tag":25,"props":722,"children":723},{},[724],{"type":29,"value":725},"Before starting another agent, ask whether the tasks are truly independent. One agent defining a new API while another writes tests against the old API is likely to create rework. A better split may be “agree on the contract first, then implement producer and consumer in separate branches.” Parallel execution is a scheduling tool, not a substitute for shared design.",{"type":24,"tag":25,"props":727,"children":728},{},[729],{"type":29,"value":730},"It also helps to keep one owner for integration. Agents can propose commits and report their checks, but the integrator should review the combined behavior, resolve conflicts, and run the full verification after combining changes. A passing build in each branch does not prove the combination passes.",{"type":24,"tag":50,"props":732,"children":734},{"id":733},"integrate-one-result-at-a-time",[735],{"type":29,"value":736},"Integrate one result at a time",{"type":24,"tag":25,"props":738,"children":739},{},[740,742,747,749,754,756,761],{"type":29,"value":741},"Assume ",{"type":24,"tag":14,"props":743,"children":745},{"className":744},[],[746],{"type":29,"value":82},{"type":29,"value":748}," is ready. Starting in ",{"type":24,"tag":14,"props":750,"children":752},{"className":751},[],[753],{"type":29,"value":179},{"type":29,"value":755},", check its diff and verification results in its worktree, then update it onto the latest ",{"type":24,"tag":14,"props":757,"children":759},{"className":758},[],[760],{"type":29,"value":74},{"type":29,"value":762},":",{"type":24,"tag":105,"props":764,"children":766},{"className":222,"code":765,"language":224,"meta":7,"style":7},"cd ../.worktrees/project/search-index\ngit status --short --branch\ngit diff main...HEAD\ngit rebase main\n# Run the project's required checks again after the rebase.\n",[767],{"type":24,"tag":14,"props":768,"children":769},{"__ignoreMap":7},[770,781,800,817,833],{"type":24,"tag":114,"props":771,"children":772},{"class":116,"line":117},[773,777],{"type":24,"tag":114,"props":774,"children":775},{"style":240},[776],{"type":29,"value":427},{"type":24,"tag":114,"props":778,"children":779},{"style":246},[780],{"type":29,"value":432},{"type":24,"tag":114,"props":782,"children":783},{"class":116,"line":126},[784,788,792,796],{"type":24,"tag":114,"props":785,"children":786},{"style":234},[787],{"type":29,"value":16},{"type":24,"tag":114,"props":789,"children":790},{"style":246},[791],{"type":29,"value":444},{"type":24,"tag":114,"props":793,"children":794},{"style":240},[795],{"type":29,"value":449},{"type":24,"tag":114,"props":797,"children":798},{"style":240},[799],{"type":29,"value":454},{"type":24,"tag":114,"props":801,"children":802},{"class":116,"line":135},[803,807,812],{"type":24,"tag":114,"props":804,"children":805},{"style":234},[806],{"type":29,"value":16},{"type":24,"tag":114,"props":808,"children":809},{"style":246},[810],{"type":29,"value":811}," diff",{"type":24,"tag":114,"props":813,"children":814},{"style":246},[815],{"type":29,"value":816}," main...HEAD\n",{"type":24,"tag":114,"props":818,"children":819},{"class":116,"line":144},[820,824,829],{"type":24,"tag":114,"props":821,"children":822},{"style":234},[823],{"type":29,"value":16},{"type":24,"tag":114,"props":825,"children":826},{"style":246},[827],{"type":29,"value":828}," rebase",{"type":24,"tag":114,"props":830,"children":831},{"style":246},[832],{"type":29,"value":286},{"type":24,"tag":114,"props":834,"children":835},{"class":116,"line":153},[836],{"type":24,"tag":114,"props":837,"children":838},{"style":460},[839],{"type":29,"value":840},"# Run the project's required checks again after the rebase.\n",{"type":24,"tag":25,"props":842,"children":843},{},[844,850,852,858,859,865],{"type":24,"tag":14,"props":845,"children":847},{"className":846},[],[848],{"type":29,"value":849},"git diff main...HEAD",{"type":29,"value":851}," shows committed changes on the task branch relative to the common ancestor. Review uncommitted changes separately with ",{"type":24,"tag":14,"props":853,"children":855},{"className":854},[],[856],{"type":29,"value":857},"git diff",{"type":29,"value":379},{"type":24,"tag":14,"props":860,"children":862},{"className":861},[],[863],{"type":29,"value":864},"git diff --cached",{"type":29,"value":866},"; do not assume the three-dot diff includes them. Rebase only a branch whose history your workflow permits you to rewrite. If it is shared with other people, agree on an update strategy before rewriting it.",{"type":24,"tag":25,"props":868,"children":869},{},[870],{"type":29,"value":871},"When the branch passes, integrate according to your repository's policy. For a team using squash commits, the main checkout can do this:",{"type":24,"tag":105,"props":873,"children":875},{"className":222,"code":874,"language":224,"meta":7,"style":7},"cd ../../../project\ngit merge --squash feature/search-index\ngit commit -m \"perf: speed up order search\"\n",[876],{"type":24,"tag":14,"props":877,"children":878},{"__ignoreMap":7},[879,891,913],{"type":24,"tag":114,"props":880,"children":881},{"class":116,"line":117},[882,886],{"type":24,"tag":114,"props":883,"children":884},{"style":240},[885],{"type":29,"value":427},{"type":24,"tag":114,"props":887,"children":888},{"style":246},[889],{"type":29,"value":890}," ../../../project\n",{"type":24,"tag":114,"props":892,"children":893},{"class":116,"line":126},[894,898,903,908],{"type":24,"tag":114,"props":895,"children":896},{"style":234},[897],{"type":29,"value":16},{"type":24,"tag":114,"props":899,"children":900},{"style":246},[901],{"type":29,"value":902}," merge",{"type":24,"tag":114,"props":904,"children":905},{"style":240},[906],{"type":29,"value":907}," --squash",{"type":24,"tag":114,"props":909,"children":910},{"style":246},[911],{"type":29,"value":912}," feature/search-index\n",{"type":24,"tag":114,"props":914,"children":915},{"class":116,"line":135},[916,920,925,930],{"type":24,"tag":114,"props":917,"children":918},{"style":234},[919],{"type":29,"value":16},{"type":24,"tag":114,"props":921,"children":922},{"style":246},[923],{"type":29,"value":924}," commit",{"type":24,"tag":114,"props":926,"children":927},{"style":240},[928],{"type":29,"value":929}," -m",{"type":24,"tag":114,"props":931,"children":932},{"style":246},[933],{"type":29,"value":934}," \"perf: speed up order search\"\n",{"type":24,"tag":25,"props":936,"children":937},{},[938,940,945,947,953,955,960,962,967],{"type":29,"value":939},"The relative ",{"type":24,"tag":14,"props":941,"children":943},{"className":942},[],[944],{"type":29,"value":427},{"type":29,"value":946}," above assumes the exact layout shown earlier and starts in ",{"type":24,"tag":14,"props":948,"children":950},{"className":949},[],[951],{"type":29,"value":952},"search-index/",{"type":29,"value":954},"; adjust it if you chose another directory. Squash creates one integration commit on ",{"type":24,"tag":14,"props":956,"children":958},{"className":957},[],[959],{"type":29,"value":74},{"type":29,"value":961},". A normal merge or pull request can be equally appropriate if that is your team's policy. The point is to integrate one reviewed result, then update the next branch against the new ",{"type":24,"tag":14,"props":963,"children":965},{"className":964},[],[966],{"type":29,"value":74},{"type":29,"value":103},{"type":24,"tag":25,"props":969,"children":970},{},[971,973,978,980,985,987,992],{"type":29,"value":972},"Now ",{"type":24,"tag":14,"props":974,"children":976},{"className":975},[],[977],{"type":29,"value":90},{"type":29,"value":979}," still starts from the old ",{"type":24,"tag":14,"props":981,"children":983},{"className":982},[],[984],{"type":29,"value":74},{"type":29,"value":986},". Rebase it, resolve any overlap, and rerun its checks before integrating it. Finally run the combined repository checks on ",{"type":24,"tag":14,"props":988,"children":990},{"className":989},[],[991],{"type":29,"value":74},{"type":29,"value":993},". Conflicts are useful information: they show that the tasks were coupled at a code boundary you may want to improve next time.",{"type":24,"tag":50,"props":995,"children":997},{"id":996},"clean-up-without-losing-work",[998],{"type":29,"value":999},"Clean up without losing work",{"type":24,"tag":25,"props":1001,"children":1002},{},[1003],{"type":29,"value":1004},"Once a task has been integrated and you have checked that nothing remains uncommitted, retire its checkout:",{"type":24,"tag":105,"props":1006,"children":1008},{"className":222,"code":1007,"language":224,"meta":7,"style":7},"git worktree list\ngit worktree remove ../.worktrees/project/search-index\n",[1009],{"type":24,"tag":14,"props":1010,"children":1011},{"__ignoreMap":7},[1012,1027],{"type":24,"tag":114,"props":1013,"children":1014},{"class":116,"line":117},[1015,1019,1023],{"type":24,"tag":114,"props":1016,"children":1017},{"style":234},[1018],{"type":29,"value":16},{"type":24,"tag":114,"props":1020,"children":1021},{"style":246},[1022],{"type":29,"value":261},{"type":24,"tag":114,"props":1024,"children":1025},{"style":246},[1026],{"type":29,"value":335},{"type":24,"tag":114,"props":1028,"children":1029},{"class":116,"line":126},[1030,1034,1038,1043],{"type":24,"tag":114,"props":1031,"children":1032},{"style":234},[1033],{"type":29,"value":16},{"type":24,"tag":114,"props":1035,"children":1036},{"style":246},[1037],{"type":29,"value":261},{"type":24,"tag":114,"props":1039,"children":1040},{"style":246},[1041],{"type":29,"value":1042}," remove",{"type":24,"tag":114,"props":1044,"children":1045},{"style":246},[1046],{"type":29,"value":432},{"type":24,"tag":25,"props":1048,"children":1049},{},[1050,1052,1057,1059,1065,1067,1073,1075,1080,1082,1088,1090,1096],{"type":29,"value":1051},"Run those commands from the main ",{"type":24,"tag":14,"props":1053,"children":1055},{"className":1054},[],[1056],{"type":29,"value":179},{"type":29,"value":1058}," checkout. ",{"type":24,"tag":14,"props":1060,"children":1062},{"className":1061},[],[1063],{"type":29,"value":1064},"git worktree remove",{"type":29,"value":1066}," refuses to remove a dirty worktree by default. Do not use ",{"type":24,"tag":14,"props":1068,"children":1070},{"className":1069},[],[1071],{"type":29,"value":1072},"--force",{"type":29,"value":1074}," as a routine cleanup step. If you used a squash merge, Git may not consider the original branch “merged” because its commit is not literally in ",{"type":24,"tag":14,"props":1076,"children":1078},{"className":1077},[],[1079],{"type":29,"value":74},{"type":29,"value":1081},"; after verifying the squash contains the intended changes, delete that local branch with ",{"type":24,"tag":14,"props":1083,"children":1085},{"className":1084},[],[1086],{"type":29,"value":1087},"git branch -D feature/search-index",{"type":29,"value":1089},". With a normal merge, ",{"type":24,"tag":14,"props":1091,"children":1093},{"className":1092},[],[1094],{"type":29,"value":1095},"git branch -d",{"type":29,"value":1097}," can check ancestry for you.",{"type":24,"tag":25,"props":1099,"children":1100},{},[1101,1103,1108,1110,1115,1117,1123,1125,1131],{"type":29,"value":1102},"Avoid deleting a worktree directory by hand. Git retains administrative records for linked worktrees; use ",{"type":24,"tag":14,"props":1104,"children":1106},{"className":1105},[],[1107],{"type":29,"value":1064},{"type":29,"value":1109},". If someone has already deleted a directory, inspect ",{"type":24,"tag":14,"props":1111,"children":1113},{"className":1112},[],[1114],{"type":29,"value":187},{"type":29,"value":1116}," and use ",{"type":24,"tag":14,"props":1118,"children":1120},{"className":1119},[],[1121],{"type":29,"value":1122},"git worktree prune",{"type":29,"value":1124}," only for stale entries. The ",{"type":24,"tag":94,"props":1126,"children":1128},{"href":365,"rel":1127},[98],[1129],{"type":29,"value":1130},"worktree cleanup documentation",{"type":29,"value":1132}," explains these commands.",{"type":24,"tag":50,"props":1134,"children":1136},{"id":1135},"a-small-operating-checklist",[1137],{"type":29,"value":1138},"A small operating checklist",{"type":24,"tag":25,"props":1140,"children":1141},{},[1142],{"type":29,"value":1143},"The practical habit is simple:",{"type":24,"tag":1145,"props":1146,"children":1147},"ol",{},[1148,1154,1159,1164,1169],{"type":24,"tag":1149,"props":1150,"children":1151},"li",{},[1152],{"type":29,"value":1153},"Start each task from a known commit on its own branch and worktree.",{"type":24,"tag":1149,"props":1155,"children":1156},{},[1157],{"type":29,"value":1158},"Assign one person or agent to that worktree and keep the task narrow.",{"type":24,"tag":1149,"props":1160,"children":1161},{},[1162],{"type":29,"value":1163},"Isolate dependencies, ports, caches, and local data that could collide.",{"type":24,"tag":1149,"props":1165,"children":1166},{},[1167],{"type":29,"value":1168},"Review the diff and checks in each branch; integrate sequentially.",{"type":24,"tag":1149,"props":1170,"children":1171},{},[1172],{"type":29,"value":1173},"Verify the combined result and remove finished worktrees.",{"type":24,"tag":25,"props":1175,"children":1176},{},[1177],{"type":29,"value":1178},"Worktrees make it cheap to keep several contexts open at once. AI makes producing changes in those contexts faster. The engineering value comes from keeping each change understandable, testable, and safe to combine.",{"type":24,"tag":50,"props":1180,"children":1182},{"id":1181},"further-reading",[1183],{"type":29,"value":1184},"Further reading",{"type":24,"tag":1186,"props":1187,"children":1188},"ul",{},[1189,1199,1210],{"type":24,"tag":1149,"props":1190,"children":1191},{},[1192,1198],{"type":24,"tag":94,"props":1193,"children":1195},{"href":96,"rel":1194},[98],[1196],{"type":29,"value":1197},"Git: git-worktree documentation",{"type":29,"value":103},{"type":24,"tag":1149,"props":1200,"children":1201},{},[1202,1209],{"type":24,"tag":94,"props":1203,"children":1206},{"href":1204,"rel":1205},"https://git-scm.com/docs/git-rebase",[98],[1207],{"type":29,"value":1208},"Git: git-rebase documentation",{"type":29,"value":103},{"type":24,"tag":1149,"props":1211,"children":1212},{},[1213,1220],{"type":24,"tag":94,"props":1214,"children":1217},{"href":1215,"rel":1216},"https://git-scm.com/docs/git-merge",[98],[1218],{"type":29,"value":1219},"Git: git-merge documentation",{"type":29,"value":103},{"type":24,"tag":1222,"props":1223,"children":1224},"style",{},[1225],{"type":29,"value":1226},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":7,"searchDepth":126,"depth":126,"links":1228},[1229,1230,1231,1232,1233,1234,1235,1236],{"id":52,"depth":126,"text":55},{"id":197,"depth":126,"text":200},{"id":486,"depth":126,"text":489},{"id":668,"depth":126,"text":671},{"id":733,"depth":126,"text":736},{"id":996,"depth":126,"text":999},{"id":1135,"depth":126,"text":1138},{"id":1181,"depth":126,"text":1184},"markdown","content:posts:git-worktrees-parallel-development.md","content","posts/git-worktrees-parallel-development.md","posts/git-worktrees-parallel-development","md",[1244,3312,5140],{"_path":1245,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1246,"description":1247,"layout":10,"date":1248,"subtitle":1249,"image":1250,"optimized_image":1250,"category":14,"tags":1251,"author":19,"paginate":6,"body":1256,"_type":1237,"_id":3309,"_source":1239,"_file":3310,"_stem":3311,"_extension":1242},"/posts/transactional-outbox","Transactional Outbox: Saving State Without Losing Events","Understand the database-and-broker dual-write problem, implement a transactional outbox, and handle delivery retries, duplicate events, ordering, and recovery.","2025-06-15T11:00:00.000Z","Keep database changes and the intent to publish in the same transaction","/assets/img/uploads/transactional-outbox.jpg",[14,1252,1253,1254,1255],"architecture","events","postgresql","microservices",{"type":21,"children":1257,"toc":3295},[1258,1271,1276,1286,1291,1304,1310,1315,1338,1343,1348,1453,1466,1480,1494,1500,1505,1510,1515,1520,1526,1531,1594,1615,1620,1913,1949,2197,2216,2229,2258,2270,2276,2281,2286,2415,2427,2507,2512,2532,2537,2542,2548,2553,2666,2678,2683,2689,2694,2800,2835,3012,3025,3030,3035,3041,3046,3067,3072,3086,3092,3118,3123,3128,3133,3139,3144,3149,3154,3159,3164,3170,3175,3208,3213,3219,3224,3229,3234,3238,3291],{"type":24,"tag":25,"props":1259,"children":1260},{},[1261,1263,1269],{"type":29,"value":1262},"Imagine an order service that saves a new order in PostgreSQL and publishes ",{"type":24,"tag":14,"props":1264,"children":1266},{"className":1265},[],[1267],{"type":29,"value":1268},"OrderCreated",{"type":29,"value":1270},". Other services use that event to start fulfillment, update search results, and build the customer's order history. The order is safely stored, but the process crashes before publishing the event.",{"type":24,"tag":25,"props":1272,"children":1273},{},[1274],{"type":29,"value":1275},"From the order service's perspective, the write succeeded. From the rest of the system's perspective, the order never appeared. The broker can be healthy, consumers can be running, and their lag can be zero. None of that helps: there is no message for them to process.",{"type":24,"tag":25,"props":1277,"children":1278},{},[1279,1284],{"type":24,"tag":37,"props":1280,"children":1281},{},[1282],{"type":29,"value":1283},"When a system relies on events to keep its services consistent, publishing those events is part of completing the business operation.",{"type":29,"value":1285}," Losing one can leave a workflow stuck, a projection stale, or a user waiting for something that will never happen automatically.",{"type":24,"tag":25,"props":1287,"children":1288},{},[1289],{"type":29,"value":1290},"This is why Transactional Outbox matters. It gives the service a durable record of what still needs to be published, committed together with the business change. Understanding the pattern also means understanding its limits: delivery can be delayed or repeated, and the rest of the pipeline still needs a recovery strategy.",{"type":24,"tag":25,"props":1292,"children":1293},{},[1294,1296,1302],{"type":29,"value":1295},"The order workflow below is illustrative. It builds on the operational concerns discussed in ",{"type":24,"tag":94,"props":1297,"children":1299},{"href":1298},"/hidden-cost-event-driven-architectures/",[1300],{"type":29,"value":1301},"The Hidden Cost of Event-Driven Architectures",{"type":29,"value":1303},", concentrating on the boundary between a database commit and event publication.",{"type":24,"tag":50,"props":1305,"children":1307},{"id":1306},"two-successful-operations-do-not-make-one-transaction",[1308],{"type":29,"value":1309},"Two successful operations do not make one transaction",{"type":24,"tag":25,"props":1311,"children":1312},{},[1313],{"type":29,"value":1314},"The obvious implementation contains two steps:",{"type":24,"tag":105,"props":1316,"children":1318},{"className":107,"code":1317,"language":29,"meta":7,"style":7},"save order and commit database transaction\npublish OrderCreated to broker\n",[1319],{"type":24,"tag":14,"props":1320,"children":1321},{"__ignoreMap":7},[1322,1330],{"type":24,"tag":114,"props":1323,"children":1324},{"class":116,"line":117},[1325],{"type":24,"tag":114,"props":1326,"children":1327},{},[1328],{"type":29,"value":1329},"save order and commit database transaction\n",{"type":24,"tag":114,"props":1331,"children":1332},{"class":116,"line":126},[1333],{"type":24,"tag":114,"props":1334,"children":1335},{},[1336],{"type":29,"value":1337},"publish OrderCreated to broker\n",{"type":24,"tag":25,"props":1339,"children":1340},{},[1341],{"type":29,"value":1342},"Those steps belong to different systems. A normal database transaction cannot roll back a message that an independent broker already accepted. Likewise, a broker transaction does not automatically include a PostgreSQL commit.",{"type":24,"tag":25,"props":1344,"children":1345},{},[1346],{"type":29,"value":1347},"Changing the order only changes the failure window:",{"type":24,"tag":1349,"props":1350,"children":1351},"table",{},[1352,1376],{"type":24,"tag":1353,"props":1354,"children":1355},"thead",{},[1356],{"type":24,"tag":1357,"props":1358,"children":1359},"tr",{},[1360,1366,1371],{"type":24,"tag":1361,"props":1362,"children":1363},"th",{},[1364],{"type":29,"value":1365},"Sequence",{"type":24,"tag":1361,"props":1367,"children":1368},{},[1369],{"type":29,"value":1370},"Failure window",{"type":24,"tag":1361,"props":1372,"children":1373},{},[1374],{"type":29,"value":1375},"Result",{"type":24,"tag":1377,"props":1378,"children":1379},"tbody",{},[1380,1399,1417,1435],{"type":24,"tag":1357,"props":1381,"children":1382},{},[1383,1389,1394],{"type":24,"tag":1384,"props":1385,"children":1386},"td",{},[1387],{"type":29,"value":1388},"Commit the order, then publish",{"type":24,"tag":1384,"props":1390,"children":1391},{},[1392],{"type":29,"value":1393},"Process dies after the commit but before publication",{"type":24,"tag":1384,"props":1395,"children":1396},{},[1397],{"type":29,"value":1398},"The order exists; its event is missing",{"type":24,"tag":1357,"props":1400,"children":1401},{},[1402,1407,1412],{"type":24,"tag":1384,"props":1403,"children":1404},{},[1405],{"type":29,"value":1406},"Publish, then commit the order",{"type":24,"tag":1384,"props":1408,"children":1409},{},[1410],{"type":29,"value":1411},"Publication succeeds, but the database transaction rolls back",{"type":24,"tag":1384,"props":1413,"children":1414},{},[1415],{"type":29,"value":1416},"Consumers receive a fact that was never committed",{"type":24,"tag":1357,"props":1418,"children":1419},{},[1420,1425,1430],{"type":24,"tag":1384,"props":1421,"children":1422},{},[1423],{"type":29,"value":1424},"Publish while the database transaction is open",{"type":24,"tag":1384,"props":1426,"children":1427},{},[1428],{"type":29,"value":1429},"Broker accepts the event; a later database operation or commit fails",{"type":24,"tag":1384,"props":1431,"children":1432},{},[1433],{"type":29,"value":1434},"The external side effect survives the database rollback",{"type":24,"tag":1357,"props":1436,"children":1437},{},[1438,1443,1448],{"type":24,"tag":1384,"props":1439,"children":1440},{},[1441],{"type":29,"value":1442},"Commit, publish, and retry after a timeout",{"type":24,"tag":1384,"props":1444,"children":1445},{},[1446],{"type":29,"value":1447},"Broker accepted the message, but its acknowledgement was lost",{"type":24,"tag":1384,"props":1449,"children":1450},{},[1451],{"type":29,"value":1452},"Retrying can publish the same event again",{"type":24,"tag":25,"props":1454,"children":1455},{},[1456,1458,1464],{"type":29,"value":1457},"A ",{"type":24,"tag":14,"props":1459,"children":1461},{"className":1460},[],[1462],{"type":29,"value":1463},"try/catch",{"type":29,"value":1465}," cannot run after the process has disappeared. An in-memory retry queue disappears with it. Writing a retry record only after publication fails still leaves a crash window before that record is saved.",{"type":24,"tag":25,"props":1467,"children":1468},{},[1469,1471,1478],{"type":29,"value":1470},"Publishing in an after-commit callback avoids announcing rolled-back state, but it does not make the callback durable. A crash can still happen between the database commit and the callback completing. Spring's ",{"type":24,"tag":94,"props":1472,"children":1475},{"href":1473,"rel":1474},"https://docs.spring.io/spring-framework/reference/data-access/transaction/event.html",[98],[1476],{"type":29,"value":1477},"transaction-bound event listeners",{"type":29,"value":1479}," let you choose a transaction phase; that phase selection alone does not create a persistent delivery mechanism.",{"type":24,"tag":25,"props":1481,"children":1482},{},[1483,1485,1492],{"type":29,"value":1484},"Distributed transactions can coordinate resources where the infrastructure supports them, but bring their own coupling and operational constraints. The outbox approach uses one local transaction and asynchronous delivery instead. Chris Richardson's ",{"type":24,"tag":94,"props":1486,"children":1489},{"href":1487,"rel":1488},"https://microservices.io/patterns/data/transactional-outbox.html",[98],[1490],{"type":29,"value":1491},"Transactional Outbox pattern",{"type":29,"value":1493}," describes this trade-off.",{"type":24,"tag":50,"props":1495,"children":1497},{"id":1496},"why-a-missing-event-can-become-permanent-inconsistency",[1498],{"type":29,"value":1499},"Why a missing event can become permanent inconsistency",{"type":24,"tag":25,"props":1501,"children":1502},{},[1503],{"type":29,"value":1504},"A delayed event and a lost event are different problems. If an event remains durably pending, a recovering publisher can eventually deliver it. If nothing recorded the obligation to publish, waiting longer does not recreate that obligation.",{"type":24,"tag":25,"props":1506,"children":1507},{},[1508],{"type":29,"value":1509},"For the order example, the consequences could include an order absent from the customer-facing search index, fulfillment never starting, or a dependent workflow remaining pending indefinitely. Different services can give different answers about the same operation. Support teams then have to identify the missing transition and decide which actions need replaying.",{"type":24,"tag":25,"props":1511,"children":1512},{},[1513],{"type":29,"value":1514},"This is especially serious when consumers maintain their own state rather than consulting the source service on every request. Eventual consistency depends on a mechanism that actually propagates the required changes. It does not mean the system will converge simply because enough time has passed.",{"type":24,"tag":25,"props":1516,"children":1517},{},[1518],{"type":29,"value":1519},"Reconciliation jobs can detect some discrepancies, but reconstructing a historical event from today's row is not always possible. The row may have changed several times since the missing transition. Record the intended event when the transition happens.",{"type":24,"tag":50,"props":1521,"children":1523},{"id":1522},"commit-the-business-change-and-the-publication-intent-together",[1524],{"type":29,"value":1525},"Commit the business change and the publication intent together",{"type":24,"tag":25,"props":1527,"children":1528},{},[1529],{"type":29,"value":1530},"An outbox is a table in the same transactional database as the business data. The application writes both in one transaction. A separate publisher, often called a relay, reads committed outbox entries and sends them to the broker.",{"type":24,"tag":105,"props":1532,"children":1534},{"className":107,"code":1533,"language":29,"meta":7,"style":7},"Application transaction in PostgreSQL\n    insert order\n    insert outbox event\n    commit both\n            |\n            v\nRelay reads committed event -> Broker -> Consumers\n",[1535],{"type":24,"tag":14,"props":1536,"children":1537},{"__ignoreMap":7},[1538,1546,1554,1562,1570,1578,1586],{"type":24,"tag":114,"props":1539,"children":1540},{"class":116,"line":117},[1541],{"type":24,"tag":114,"props":1542,"children":1543},{},[1544],{"type":29,"value":1545},"Application transaction in PostgreSQL\n",{"type":24,"tag":114,"props":1547,"children":1548},{"class":116,"line":126},[1549],{"type":24,"tag":114,"props":1550,"children":1551},{},[1552],{"type":29,"value":1553},"    insert order\n",{"type":24,"tag":114,"props":1555,"children":1556},{"class":116,"line":135},[1557],{"type":24,"tag":114,"props":1558,"children":1559},{},[1560],{"type":29,"value":1561},"    insert outbox event\n",{"type":24,"tag":114,"props":1563,"children":1564},{"class":116,"line":144},[1565],{"type":24,"tag":114,"props":1566,"children":1567},{},[1568],{"type":29,"value":1569},"    commit both\n",{"type":24,"tag":114,"props":1571,"children":1572},{"class":116,"line":153},[1573],{"type":24,"tag":114,"props":1574,"children":1575},{},[1576],{"type":29,"value":1577},"            |\n",{"type":24,"tag":114,"props":1579,"children":1580},{"class":116,"line":162},[1581],{"type":24,"tag":114,"props":1582,"children":1583},{},[1584],{"type":29,"value":1585},"            v\n",{"type":24,"tag":114,"props":1587,"children":1588},{"class":116,"line":609},[1589],{"type":24,"tag":114,"props":1590,"children":1591},{},[1592],{"type":29,"value":1593},"Relay reads committed event -> Broker -> Consumers\n",{"type":24,"tag":25,"props":1595,"children":1596},{},[1597,1599,1604,1606,1613],{"type":29,"value":1598},"The atomic boundary is ",{"type":24,"tag":37,"props":1600,"children":1601},{},[1602],{"type":29,"value":1603},"business state plus durable event intent",{"type":29,"value":1605},". It does not include the broker or the consumers. PostgreSQL's ",{"type":24,"tag":94,"props":1607,"children":1610},{"href":1608,"rel":1609},"https://www.postgresql.org/docs/current/tutorial-transactions.html",[98],[1611],{"type":29,"value":1612},"transaction model",{"type":29,"value":1614}," makes the two writes commit or roll back together, provided they actually participate in the same transaction.",{"type":24,"tag":25,"props":1616,"children":1617},{},[1618],{"type":29,"value":1619},"Here is a small schema for a polling implementation:",{"type":24,"tag":105,"props":1621,"children":1625},{"className":1622,"code":1623,"language":1624,"meta":7,"style":7},"language-sql shiki shiki-themes github-dark github-light","CREATE TABLE outbox_event (\n    event_id UUID PRIMARY KEY,\n    aggregate_type TEXT NOT NULL,\n    aggregate_id TEXT NOT NULL,\n    aggregate_version BIGINT NOT NULL,\n    event_type TEXT NOT NULL,\n    schema_version INTEGER NOT NULL,\n    payload JSONB NOT NULL,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    published_at TIMESTAMPTZ\n);\n\nCREATE INDEX outbox_event_pending_idx\n    ON outbox_event (created_at, event_id)\n    WHERE published_at IS NULL;\n","sql",[1626],{"type":24,"tag":14,"props":1627,"children":1628},{"__ignoreMap":7},[1629,1654,1672,1694,1714,1735,1755,1776,1793,1821,1835,1844,1852,1870,1884],{"type":24,"tag":114,"props":1630,"children":1631},{"class":116,"line":117},[1632,1638,1643,1648],{"type":24,"tag":114,"props":1633,"children":1635},{"style":1634},"--shiki-default:#F97583;--shiki-light:#D73A49",[1636],{"type":29,"value":1637},"CREATE",{"type":24,"tag":114,"props":1639,"children":1640},{"style":1634},[1641],{"type":29,"value":1642}," TABLE",{"type":24,"tag":114,"props":1644,"children":1645},{"style":234},[1646],{"type":29,"value":1647}," outbox_event",{"type":24,"tag":114,"props":1649,"children":1651},{"style":1650},"--shiki-default:#E1E4E8;--shiki-light:#24292E",[1652],{"type":29,"value":1653}," (\n",{"type":24,"tag":114,"props":1655,"children":1656},{"class":116,"line":126},[1657,1662,1667],{"type":24,"tag":114,"props":1658,"children":1659},{"style":1650},[1660],{"type":29,"value":1661},"    event_id UUID ",{"type":24,"tag":114,"props":1663,"children":1664},{"style":1634},[1665],{"type":29,"value":1666},"PRIMARY KEY",{"type":24,"tag":114,"props":1668,"children":1669},{"style":1650},[1670],{"type":29,"value":1671},",\n",{"type":24,"tag":114,"props":1673,"children":1674},{"class":116,"line":135},[1675,1680,1685,1690],{"type":24,"tag":114,"props":1676,"children":1677},{"style":1650},[1678],{"type":29,"value":1679},"    aggregate_type ",{"type":24,"tag":114,"props":1681,"children":1682},{"style":1634},[1683],{"type":29,"value":1684},"TEXT",{"type":24,"tag":114,"props":1686,"children":1687},{"style":1634},[1688],{"type":29,"value":1689}," NOT NULL",{"type":24,"tag":114,"props":1691,"children":1692},{"style":1650},[1693],{"type":29,"value":1671},{"type":24,"tag":114,"props":1695,"children":1696},{"class":116,"line":144},[1697,1702,1706,1710],{"type":24,"tag":114,"props":1698,"children":1699},{"style":1650},[1700],{"type":29,"value":1701},"    aggregate_id ",{"type":24,"tag":114,"props":1703,"children":1704},{"style":1634},[1705],{"type":29,"value":1684},{"type":24,"tag":114,"props":1707,"children":1708},{"style":1634},[1709],{"type":29,"value":1689},{"type":24,"tag":114,"props":1711,"children":1712},{"style":1650},[1713],{"type":29,"value":1671},{"type":24,"tag":114,"props":1715,"children":1716},{"class":116,"line":153},[1717,1722,1727,1731],{"type":24,"tag":114,"props":1718,"children":1719},{"style":1650},[1720],{"type":29,"value":1721},"    aggregate_version ",{"type":24,"tag":114,"props":1723,"children":1724},{"style":1634},[1725],{"type":29,"value":1726},"BIGINT",{"type":24,"tag":114,"props":1728,"children":1729},{"style":1634},[1730],{"type":29,"value":1689},{"type":24,"tag":114,"props":1732,"children":1733},{"style":1650},[1734],{"type":29,"value":1671},{"type":24,"tag":114,"props":1736,"children":1737},{"class":116,"line":162},[1738,1743,1747,1751],{"type":24,"tag":114,"props":1739,"children":1740},{"style":1650},[1741],{"type":29,"value":1742},"    event_type ",{"type":24,"tag":114,"props":1744,"children":1745},{"style":1634},[1746],{"type":29,"value":1684},{"type":24,"tag":114,"props":1748,"children":1749},{"style":1634},[1750],{"type":29,"value":1689},{"type":24,"tag":114,"props":1752,"children":1753},{"style":1650},[1754],{"type":29,"value":1671},{"type":24,"tag":114,"props":1756,"children":1757},{"class":116,"line":609},[1758,1763,1768,1772],{"type":24,"tag":114,"props":1759,"children":1760},{"style":1650},[1761],{"type":29,"value":1762},"    schema_version ",{"type":24,"tag":114,"props":1764,"children":1765},{"style":1634},[1766],{"type":29,"value":1767},"INTEGER",{"type":24,"tag":114,"props":1769,"children":1770},{"style":1634},[1771],{"type":29,"value":1689},{"type":24,"tag":114,"props":1773,"children":1774},{"style":1650},[1775],{"type":29,"value":1671},{"type":24,"tag":114,"props":1777,"children":1778},{"class":116,"line":621},[1779,1784,1789],{"type":24,"tag":114,"props":1780,"children":1781},{"style":1650},[1782],{"type":29,"value":1783},"    payload JSONB ",{"type":24,"tag":114,"props":1785,"children":1786},{"style":1634},[1787],{"type":29,"value":1788},"NOT NULL",{"type":24,"tag":114,"props":1790,"children":1791},{"style":1650},[1792],{"type":29,"value":1671},{"type":24,"tag":114,"props":1794,"children":1796},{"class":116,"line":1795},9,[1797,1802,1807,1811,1816],{"type":24,"tag":114,"props":1798,"children":1799},{"style":1650},[1800],{"type":29,"value":1801},"    created_at ",{"type":24,"tag":114,"props":1803,"children":1804},{"style":1634},[1805],{"type":29,"value":1806},"TIMESTAMPTZ",{"type":24,"tag":114,"props":1808,"children":1809},{"style":1634},[1810],{"type":29,"value":1689},{"type":24,"tag":114,"props":1812,"children":1813},{"style":1634},[1814],{"type":29,"value":1815}," DEFAULT",{"type":24,"tag":114,"props":1817,"children":1818},{"style":1650},[1819],{"type":29,"value":1820}," CURRENT_TIMESTAMP,\n",{"type":24,"tag":114,"props":1822,"children":1824},{"class":116,"line":1823},10,[1825,1830],{"type":24,"tag":114,"props":1826,"children":1827},{"style":1650},[1828],{"type":29,"value":1829},"    published_at ",{"type":24,"tag":114,"props":1831,"children":1832},{"style":1634},[1833],{"type":29,"value":1834},"TIMESTAMPTZ\n",{"type":24,"tag":114,"props":1836,"children":1838},{"class":116,"line":1837},11,[1839],{"type":24,"tag":114,"props":1840,"children":1841},{"style":1650},[1842],{"type":29,"value":1843},");\n",{"type":24,"tag":114,"props":1845,"children":1847},{"class":116,"line":1846},12,[1848],{"type":24,"tag":114,"props":1849,"children":1850},{"emptyLinePlaceholder":583},[1851],{"type":29,"value":586},{"type":24,"tag":114,"props":1853,"children":1855},{"class":116,"line":1854},13,[1856,1860,1865],{"type":24,"tag":114,"props":1857,"children":1858},{"style":1634},[1859],{"type":29,"value":1637},{"type":24,"tag":114,"props":1861,"children":1862},{"style":1634},[1863],{"type":29,"value":1864}," INDEX",{"type":24,"tag":114,"props":1866,"children":1867},{"style":234},[1868],{"type":29,"value":1869}," outbox_event_pending_idx\n",{"type":24,"tag":114,"props":1871,"children":1873},{"class":116,"line":1872},14,[1874,1879],{"type":24,"tag":114,"props":1875,"children":1876},{"style":1634},[1877],{"type":29,"value":1878},"    ON",{"type":24,"tag":114,"props":1880,"children":1881},{"style":1650},[1882],{"type":29,"value":1883}," outbox_event (created_at, event_id)\n",{"type":24,"tag":114,"props":1885,"children":1887},{"class":116,"line":1886},15,[1888,1893,1898,1903,1908],{"type":24,"tag":114,"props":1889,"children":1890},{"style":1634},[1891],{"type":29,"value":1892},"    WHERE",{"type":24,"tag":114,"props":1894,"children":1895},{"style":1650},[1896],{"type":29,"value":1897}," published_at ",{"type":24,"tag":114,"props":1899,"children":1900},{"style":1634},[1901],{"type":29,"value":1902},"IS",{"type":24,"tag":114,"props":1904,"children":1905},{"style":1634},[1906],{"type":29,"value":1907}," NULL",{"type":24,"tag":114,"props":1909,"children":1910},{"style":1650},[1911],{"type":29,"value":1912},";\n",{"type":24,"tag":25,"props":1914,"children":1915},{},[1916,1917,1923,1925,1931,1933,1939,1941,1947],{"type":29,"value":741},{"type":24,"tag":14,"props":1918,"children":1920},{"className":1919},[],[1921],{"type":29,"value":1922},"purchase_order",{"type":29,"value":1924}," has an ",{"type":24,"tag":14,"props":1926,"children":1928},{"className":1927},[],[1929],{"type":29,"value":1930},"id",{"type":29,"value":1932}," primary key, a ",{"type":24,"tag":14,"props":1934,"children":1936},{"className":1935},[],[1937],{"type":29,"value":1938},"status",{"type":29,"value":1940},", and a ",{"type":24,"tag":14,"props":1942,"children":1944},{"className":1943},[],[1945],{"type":29,"value":1946},"version",{"type":29,"value":1948},". Creating an order and its event becomes:",{"type":24,"tag":105,"props":1950,"children":1952},{"className":1622,"code":1951,"language":1624,"meta":7,"style":7},"BEGIN;\n\nINSERT INTO purchase_order (id, status, version)\nVALUES (:order_id, 'CREATED', 1);\n\nINSERT INTO outbox_event (\n    event_id, aggregate_type, aggregate_id, aggregate_version,\n    event_type, schema_version, payload\n)\nVALUES (\n    :event_id, 'Order', :order_id, 1,\n    'OrderCreated', 1,\n    jsonb_build_object('orderId', :order_id, 'status', 'CREATED')\n);\n\nCOMMIT;\n",[1953],{"type":24,"tag":14,"props":1954,"children":1955},{"__ignoreMap":7},[1956,1968,1975,2006,2037,2044,2056,2064,2072,2079,2090,2116,2136,2170,2177,2184],{"type":24,"tag":114,"props":1957,"children":1958},{"class":116,"line":117},[1959,1964],{"type":24,"tag":114,"props":1960,"children":1961},{"style":1634},[1962],{"type":29,"value":1963},"BEGIN",{"type":24,"tag":114,"props":1965,"children":1966},{"style":1650},[1967],{"type":29,"value":1912},{"type":24,"tag":114,"props":1969,"children":1970},{"class":116,"line":126},[1971],{"type":24,"tag":114,"props":1972,"children":1973},{"emptyLinePlaceholder":583},[1974],{"type":29,"value":586},{"type":24,"tag":114,"props":1976,"children":1977},{"class":116,"line":135},[1978,1983,1988,1992,1997,2001],{"type":24,"tag":114,"props":1979,"children":1980},{"style":1634},[1981],{"type":29,"value":1982},"INSERT INTO",{"type":24,"tag":114,"props":1984,"children":1985},{"style":1650},[1986],{"type":29,"value":1987}," purchase_order (id, ",{"type":24,"tag":114,"props":1989,"children":1990},{"style":1634},[1991],{"type":29,"value":1938},{"type":24,"tag":114,"props":1993,"children":1994},{"style":1650},[1995],{"type":29,"value":1996},", ",{"type":24,"tag":114,"props":1998,"children":1999},{"style":1634},[2000],{"type":29,"value":1946},{"type":24,"tag":114,"props":2002,"children":2003},{"style":1650},[2004],{"type":29,"value":2005},")\n",{"type":24,"tag":114,"props":2007,"children":2008},{"class":116,"line":144},[2009,2014,2019,2024,2028,2033],{"type":24,"tag":114,"props":2010,"children":2011},{"style":1634},[2012],{"type":29,"value":2013},"VALUES",{"type":24,"tag":114,"props":2015,"children":2016},{"style":1650},[2017],{"type":29,"value":2018}," (:order_id, ",{"type":24,"tag":114,"props":2020,"children":2021},{"style":246},[2022],{"type":29,"value":2023},"'CREATED'",{"type":24,"tag":114,"props":2025,"children":2026},{"style":1650},[2027],{"type":29,"value":1996},{"type":24,"tag":114,"props":2029,"children":2030},{"style":240},[2031],{"type":29,"value":2032},"1",{"type":24,"tag":114,"props":2034,"children":2035},{"style":1650},[2036],{"type":29,"value":1843},{"type":24,"tag":114,"props":2038,"children":2039},{"class":116,"line":153},[2040],{"type":24,"tag":114,"props":2041,"children":2042},{"emptyLinePlaceholder":583},[2043],{"type":29,"value":586},{"type":24,"tag":114,"props":2045,"children":2046},{"class":116,"line":162},[2047,2051],{"type":24,"tag":114,"props":2048,"children":2049},{"style":1634},[2050],{"type":29,"value":1982},{"type":24,"tag":114,"props":2052,"children":2053},{"style":1650},[2054],{"type":29,"value":2055}," outbox_event (\n",{"type":24,"tag":114,"props":2057,"children":2058},{"class":116,"line":609},[2059],{"type":24,"tag":114,"props":2060,"children":2061},{"style":1650},[2062],{"type":29,"value":2063},"    event_id, aggregate_type, aggregate_id, aggregate_version,\n",{"type":24,"tag":114,"props":2065,"children":2066},{"class":116,"line":621},[2067],{"type":24,"tag":114,"props":2068,"children":2069},{"style":1650},[2070],{"type":29,"value":2071},"    event_type, schema_version, payload\n",{"type":24,"tag":114,"props":2073,"children":2074},{"class":116,"line":1795},[2075],{"type":24,"tag":114,"props":2076,"children":2077},{"style":1650},[2078],{"type":29,"value":2005},{"type":24,"tag":114,"props":2080,"children":2081},{"class":116,"line":1823},[2082,2086],{"type":24,"tag":114,"props":2083,"children":2084},{"style":1634},[2085],{"type":29,"value":2013},{"type":24,"tag":114,"props":2087,"children":2088},{"style":1650},[2089],{"type":29,"value":1653},{"type":24,"tag":114,"props":2091,"children":2092},{"class":116,"line":1837},[2093,2098,2103,2108,2112],{"type":24,"tag":114,"props":2094,"children":2095},{"style":1650},[2096],{"type":29,"value":2097},"    :event_id, ",{"type":24,"tag":114,"props":2099,"children":2100},{"style":246},[2101],{"type":29,"value":2102},"'Order'",{"type":24,"tag":114,"props":2104,"children":2105},{"style":1650},[2106],{"type":29,"value":2107},", :order_id, ",{"type":24,"tag":114,"props":2109,"children":2110},{"style":240},[2111],{"type":29,"value":2032},{"type":24,"tag":114,"props":2113,"children":2114},{"style":1650},[2115],{"type":29,"value":1671},{"type":24,"tag":114,"props":2117,"children":2118},{"class":116,"line":1846},[2119,2124,2128,2132],{"type":24,"tag":114,"props":2120,"children":2121},{"style":246},[2122],{"type":29,"value":2123},"    'OrderCreated'",{"type":24,"tag":114,"props":2125,"children":2126},{"style":1650},[2127],{"type":29,"value":1996},{"type":24,"tag":114,"props":2129,"children":2130},{"style":240},[2131],{"type":29,"value":2032},{"type":24,"tag":114,"props":2133,"children":2134},{"style":1650},[2135],{"type":29,"value":1671},{"type":24,"tag":114,"props":2137,"children":2138},{"class":116,"line":1854},[2139,2144,2149,2153,2158,2162,2166],{"type":24,"tag":114,"props":2140,"children":2141},{"style":1650},[2142],{"type":29,"value":2143},"    jsonb_build_object(",{"type":24,"tag":114,"props":2145,"children":2146},{"style":246},[2147],{"type":29,"value":2148},"'orderId'",{"type":24,"tag":114,"props":2150,"children":2151},{"style":1650},[2152],{"type":29,"value":2107},{"type":24,"tag":114,"props":2154,"children":2155},{"style":246},[2156],{"type":29,"value":2157},"'status'",{"type":24,"tag":114,"props":2159,"children":2160},{"style":1650},[2161],{"type":29,"value":1996},{"type":24,"tag":114,"props":2163,"children":2164},{"style":246},[2165],{"type":29,"value":2023},{"type":24,"tag":114,"props":2167,"children":2168},{"style":1650},[2169],{"type":29,"value":2005},{"type":24,"tag":114,"props":2171,"children":2172},{"class":116,"line":1872},[2173],{"type":24,"tag":114,"props":2174,"children":2175},{"style":1650},[2176],{"type":29,"value":1843},{"type":24,"tag":114,"props":2178,"children":2179},{"class":116,"line":1886},[2180],{"type":24,"tag":114,"props":2181,"children":2182},{"emptyLinePlaceholder":583},[2183],{"type":29,"value":586},{"type":24,"tag":114,"props":2185,"children":2187},{"class":116,"line":2186},16,[2188,2193],{"type":24,"tag":114,"props":2189,"children":2190},{"style":1634},[2191],{"type":29,"value":2192},"COMMIT",{"type":24,"tag":114,"props":2194,"children":2195},{"style":1650},[2196],{"type":29,"value":1912},{"type":24,"tag":25,"props":2198,"children":2199},{},[2200,2201,2207,2208,2214],{"type":29,"value":340},{"type":24,"tag":14,"props":2202,"children":2204},{"className":2203},[],[2205],{"type":29,"value":2206},":order_id",{"type":29,"value":379},{"type":24,"tag":14,"props":2209,"children":2211},{"className":2210},[],[2212],{"type":29,"value":2213},":event_id",{"type":29,"value":2215}," names represent application-bound parameters, not SQL to paste unchanged into a PostgreSQL console. Generate the event UUID once for this event and preserve it on every publication retry. A real payload must include the facts its consumers need to act without guessing at later database state.",{"type":24,"tag":25,"props":2217,"children":2218},{},[2219,2221,2227],{"type":29,"value":2220},"If the outbox insert fails, roll back the business write too. Do not swallow the failure and commit the order. In a Java service, verify that both repositories use the same database transaction and connection context; a separate transaction such as ",{"type":24,"tag":14,"props":2222,"children":2224},{"className":2223},[],[2225],{"type":29,"value":2226},"REQUIRES_NEW",{"type":29,"value":2228}," would defeat the atomic boundary.",{"type":24,"tag":25,"props":2230,"children":2231},{},[2232,2234,2240,2242,2248,2250,2256],{"type":29,"value":2233},"Keep three concepts separate: ",{"type":24,"tag":14,"props":2235,"children":2237},{"className":2236},[],[2238],{"type":29,"value":2239},"event_id",{"type":29,"value":2241}," identifies a particular event, ",{"type":24,"tag":14,"props":2243,"children":2245},{"className":2244},[],[2246],{"type":29,"value":2247},"schema_version",{"type":29,"value":2249}," identifies its payload contract, and ",{"type":24,"tag":14,"props":2251,"children":2253},{"className":2252},[],[2254],{"type":29,"value":2255},"aggregate_version",{"type":29,"value":2257}," describes the source entity's progression. None is a substitute for an API request idempotency key. If a client retries order creation after an ambiguous HTTP response, the command layer still needs to prevent an unintended second order.",{"type":24,"tag":25,"props":2259,"children":2260},{},[2261,2263,2268],{"type":29,"value":2262},"Store an immutable event payload. Loading the latest order and constructing ",{"type":24,"tag":14,"props":2264,"children":2266},{"className":2265},[],[2267],{"type":29,"value":1268},{"type":29,"value":2269}," later can accidentally publish facts from a different transition.",{"type":24,"tag":50,"props":2271,"children":2273},{"id":2272},"publish-from-the-outbox-and-acknowledge-in-the-right-order",[2274],{"type":29,"value":2275},"Publish from the outbox, and acknowledge in the right order",{"type":24,"tag":25,"props":2277,"children":2278},{},[2279],{"type":29,"value":2280},"A minimal polling relay can lock one pending row, publish it, wait for a broker acknowledgement, then mark it as published. This is an educational implementation, not a complete production worker.",{"type":24,"tag":25,"props":2282,"children":2283},{},[2284],{"type":29,"value":2285},"Start a database transaction and select the row:",{"type":24,"tag":105,"props":2287,"children":2289},{"className":1622,"code":2288,"language":1624,"meta":7,"style":7},"BEGIN;\n\nSELECT event_id, aggregate_type, aggregate_id, aggregate_version,\n       event_type, schema_version, payload\nFROM outbox_event\nWHERE published_at IS NULL\nORDER BY created_at, event_id\nLIMIT 1\nFOR UPDATE SKIP LOCKED;\n",[2290],{"type":24,"tag":14,"props":2291,"children":2292},{"__ignoreMap":7},[2293,2304,2311,2324,2332,2345,2366,2379,2392],{"type":24,"tag":114,"props":2294,"children":2295},{"class":116,"line":117},[2296,2300],{"type":24,"tag":114,"props":2297,"children":2298},{"style":1634},[2299],{"type":29,"value":1963},{"type":24,"tag":114,"props":2301,"children":2302},{"style":1650},[2303],{"type":29,"value":1912},{"type":24,"tag":114,"props":2305,"children":2306},{"class":116,"line":126},[2307],{"type":24,"tag":114,"props":2308,"children":2309},{"emptyLinePlaceholder":583},[2310],{"type":29,"value":586},{"type":24,"tag":114,"props":2312,"children":2313},{"class":116,"line":135},[2314,2319],{"type":24,"tag":114,"props":2315,"children":2316},{"style":1634},[2317],{"type":29,"value":2318},"SELECT",{"type":24,"tag":114,"props":2320,"children":2321},{"style":1650},[2322],{"type":29,"value":2323}," event_id, aggregate_type, aggregate_id, aggregate_version,\n",{"type":24,"tag":114,"props":2325,"children":2326},{"class":116,"line":144},[2327],{"type":24,"tag":114,"props":2328,"children":2329},{"style":1650},[2330],{"type":29,"value":2331},"       event_type, schema_version, payload\n",{"type":24,"tag":114,"props":2333,"children":2334},{"class":116,"line":153},[2335,2340],{"type":24,"tag":114,"props":2336,"children":2337},{"style":1634},[2338],{"type":29,"value":2339},"FROM",{"type":24,"tag":114,"props":2341,"children":2342},{"style":1650},[2343],{"type":29,"value":2344}," outbox_event\n",{"type":24,"tag":114,"props":2346,"children":2347},{"class":116,"line":162},[2348,2353,2357,2361],{"type":24,"tag":114,"props":2349,"children":2350},{"style":1634},[2351],{"type":29,"value":2352},"WHERE",{"type":24,"tag":114,"props":2354,"children":2355},{"style":1650},[2356],{"type":29,"value":1897},{"type":24,"tag":114,"props":2358,"children":2359},{"style":1634},[2360],{"type":29,"value":1902},{"type":24,"tag":114,"props":2362,"children":2363},{"style":1634},[2364],{"type":29,"value":2365}," NULL\n",{"type":24,"tag":114,"props":2367,"children":2368},{"class":116,"line":609},[2369,2374],{"type":24,"tag":114,"props":2370,"children":2371},{"style":1634},[2372],{"type":29,"value":2373},"ORDER BY",{"type":24,"tag":114,"props":2375,"children":2376},{"style":1650},[2377],{"type":29,"value":2378}," created_at, event_id\n",{"type":24,"tag":114,"props":2380,"children":2381},{"class":116,"line":621},[2382,2387],{"type":24,"tag":114,"props":2383,"children":2384},{"style":1634},[2385],{"type":29,"value":2386},"LIMIT",{"type":24,"tag":114,"props":2388,"children":2389},{"style":240},[2390],{"type":29,"value":2391}," 1\n",{"type":24,"tag":114,"props":2393,"children":2394},{"class":116,"line":1795},[2395,2400,2405,2410],{"type":24,"tag":114,"props":2396,"children":2397},{"style":1634},[2398],{"type":29,"value":2399},"FOR",{"type":24,"tag":114,"props":2401,"children":2402},{"style":1634},[2403],{"type":29,"value":2404}," UPDATE",{"type":24,"tag":114,"props":2406,"children":2407},{"style":1634},[2408],{"type":29,"value":2409}," SKIP",{"type":24,"tag":114,"props":2411,"children":2412},{"style":1650},[2413],{"type":29,"value":2414}," LOCKED;\n",{"type":24,"tag":25,"props":2416,"children":2417},{},[2418,2420,2425],{"type":29,"value":2419},"If there is no row, end the transaction. Otherwise, keep the lock while the application sends an envelope containing the selected fields, including the stable ",{"type":24,"tag":14,"props":2421,"children":2423},{"className":2422},[],[2424],{"type":29,"value":2239},{"type":29,"value":2426},". Only after the broker acknowledges acceptance under the required durability settings should the same transaction run:",{"type":24,"tag":105,"props":2428,"children":2430},{"className":1622,"code":2429,"language":1624,"meta":7,"style":7},"UPDATE outbox_event\nSET published_at = CURRENT_TIMESTAMP\nWHERE event_id = :event_id;\n\nCOMMIT;\n",[2431],{"type":24,"tag":14,"props":2432,"children":2433},{"__ignoreMap":7},[2434,2446,2468,2489,2496],{"type":24,"tag":114,"props":2435,"children":2436},{"class":116,"line":117},[2437,2442],{"type":24,"tag":114,"props":2438,"children":2439},{"style":1634},[2440],{"type":29,"value":2441},"UPDATE",{"type":24,"tag":114,"props":2443,"children":2444},{"style":1650},[2445],{"type":29,"value":2344},{"type":24,"tag":114,"props":2447,"children":2448},{"class":116,"line":126},[2449,2454,2458,2463],{"type":24,"tag":114,"props":2450,"children":2451},{"style":1634},[2452],{"type":29,"value":2453},"SET",{"type":24,"tag":114,"props":2455,"children":2456},{"style":1650},[2457],{"type":29,"value":1897},{"type":24,"tag":114,"props":2459,"children":2460},{"style":1634},[2461],{"type":29,"value":2462},"=",{"type":24,"tag":114,"props":2464,"children":2465},{"style":1650},[2466],{"type":29,"value":2467}," CURRENT_TIMESTAMP\n",{"type":24,"tag":114,"props":2469,"children":2470},{"class":116,"line":135},[2471,2475,2480,2484],{"type":24,"tag":114,"props":2472,"children":2473},{"style":1634},[2474],{"type":29,"value":2352},{"type":24,"tag":114,"props":2476,"children":2477},{"style":1650},[2478],{"type":29,"value":2479}," event_id ",{"type":24,"tag":114,"props":2481,"children":2482},{"style":1634},[2483],{"type":29,"value":2462},{"type":24,"tag":114,"props":2485,"children":2486},{"style":1650},[2487],{"type":29,"value":2488}," :event_id;\n",{"type":24,"tag":114,"props":2490,"children":2491},{"class":116,"line":144},[2492],{"type":24,"tag":114,"props":2493,"children":2494},{"emptyLinePlaceholder":583},[2495],{"type":29,"value":586},{"type":24,"tag":114,"props":2497,"children":2498},{"class":116,"line":153},[2499,2503],{"type":24,"tag":114,"props":2500,"children":2501},{"style":1634},[2502],{"type":29,"value":2192},{"type":24,"tag":114,"props":2504,"children":2505},{"style":1650},[2506],{"type":29,"value":1912},{"type":24,"tag":25,"props":2508,"children":2509},{},[2510],{"type":29,"value":2511},"An asynchronous producer's local enqueue or returned future is not enough: wait for the relevant broker confirmation. On failure or timeout, roll back and retry later. A timeout is ambiguous, so that retry may be a duplicate.",{"type":24,"tag":25,"props":2513,"children":2514},{},[2515,2521,2523,2530],{"type":24,"tag":14,"props":2516,"children":2518},{"className":2517},[],[2519],{"type":29,"value":2520},"SKIP LOCKED",{"type":29,"value":2522}," lets concurrent workers bypass rows already locked by other workers. PostgreSQL documents its usefulness for ",{"type":24,"tag":94,"props":2524,"children":2527},{"href":2525,"rel":2526},"https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE",[98],[2528],{"type":29,"value":2529},"queue-like tables",{"type":29,"value":2531},". It is a work-claiming mechanism, not an ordering guarantee.",{"type":24,"tag":25,"props":2533,"children":2534},{},[2535],{"type":29,"value":2536},"Holding a transaction and connection open across a network operation has a cost. Keep waits bounded and avoid large locked batches. A higher-throughput design may claim rows with an expiring lease, commit the claim, and publish outside the transaction. That design needs lease recovery, ownership checks when marking success, and protection against stale workers. A lease does not remove duplicate-delivery windows.",{"type":24,"tag":25,"props":2538,"children":2539},{},[2540],{"type":29,"value":2541},"Never mark a row as published before sending. If the process dies after that mark, the relay may permanently skip an event that never reached the broker.",{"type":24,"tag":50,"props":2543,"children":2545},{"id":2544},"the-remaining-crash-window-produces-duplicates",[2546],{"type":29,"value":2547},"The remaining crash window produces duplicates",{"type":24,"tag":25,"props":2549,"children":2550},{},[2551],{"type":29,"value":2552},"The relay still talks to two systems. The important difference is that its work is durable and repeatable:",{"type":24,"tag":1349,"props":2554,"children":2555},{},[2556,2577],{"type":24,"tag":1353,"props":2557,"children":2558},{},[2559],{"type":24,"tag":1357,"props":2560,"children":2561},{},[2562,2567,2572],{"type":24,"tag":1361,"props":2563,"children":2564},{},[2565],{"type":29,"value":2566},"Failure point",{"type":24,"tag":1361,"props":2568,"children":2569},{},[2570],{"type":29,"value":2571},"Durable state",{"type":24,"tag":1361,"props":2573,"children":2574},{},[2575],{"type":29,"value":2576},"Recovery",{"type":24,"tag":1377,"props":2578,"children":2579},{},[2580,2598,2616,2642],{"type":24,"tag":1357,"props":2581,"children":2582},{},[2583,2588,2593],{"type":24,"tag":1384,"props":2584,"children":2585},{},[2586],{"type":29,"value":2587},"Before the producer transaction commits",{"type":24,"tag":1384,"props":2589,"children":2590},{},[2591],{"type":29,"value":2592},"Neither order nor outbox event committed",{"type":24,"tag":1384,"props":2594,"children":2595},{},[2596],{"type":29,"value":2597},"Retry the command according to its idempotency policy",{"type":24,"tag":1357,"props":2599,"children":2600},{},[2601,2606,2611],{"type":24,"tag":1384,"props":2602,"children":2603},{},[2604],{"type":29,"value":2605},"After that commit, before publication",{"type":24,"tag":1384,"props":2607,"children":2608},{},[2609],{"type":29,"value":2610},"Order and pending event exist",{"type":24,"tag":1384,"props":2612,"children":2613},{},[2614],{"type":29,"value":2615},"Relay publishes after recovery",{"type":24,"tag":1357,"props":2617,"children":2618},{},[2619,2632,2637],{"type":24,"tag":1384,"props":2620,"children":2621},{},[2622,2624,2630],{"type":29,"value":2623},"After broker acceptance, before ",{"type":24,"tag":14,"props":2625,"children":2627},{"className":2626},[],[2628],{"type":29,"value":2629},"published_at",{"type":29,"value":2631}," commits",{"type":24,"tag":1384,"props":2633,"children":2634},{},[2635],{"type":29,"value":2636},"Broker may have the event; outbox still appears pending",{"type":24,"tag":1384,"props":2638,"children":2639},{},[2640],{"type":29,"value":2641},"Relay republishes the same event ID",{"type":24,"tag":1357,"props":2643,"children":2644},{},[2645,2656,2661],{"type":24,"tag":1384,"props":2646,"children":2647},{},[2648,2650,2655],{"type":29,"value":2649},"After ",{"type":24,"tag":14,"props":2651,"children":2653},{"className":2652},[],[2654],{"type":29,"value":2629},{"type":29,"value":2631},{"type":24,"tag":1384,"props":2657,"children":2658},{},[2659],{"type":29,"value":2660},"Broker has acknowledged; relay recorded success",{"type":24,"tag":1384,"props":2662,"children":2663},{},[2664],{"type":29,"value":2665},"Normal consumer delivery continues",{"type":24,"tag":25,"props":2667,"children":2668},{},[2669,2671,2676],{"type":29,"value":2670},"This is why outbox-based publication is normally designed around ",{"type":24,"tag":37,"props":2672,"children":2673},{},[2674],{"type":29,"value":2675},"at-least-once delivery",{"type":29,"value":2677},". The pattern closes the missing-intent window; it does not promise a single delivery across database, broker, and consumers.",{"type":24,"tag":25,"props":2679,"children":2680},{},[2681],{"type":29,"value":2682},"Those guarantees depend on durable storage, appropriate broker configuration, retry and recovery, and retaining pending events. A permanently broken relay or premature cleanup can still stop propagation. A published marker also says nothing about whether a consumer has completed its work.",{"type":24,"tag":50,"props":2684,"children":2686},{"id":2685},"make-the-consumers-effect-safe-to-repeat",[2687],{"type":29,"value":2688},"Make the consumer's effect safe to repeat",{"type":24,"tag":25,"props":2690,"children":2691},{},[2692],{"type":29,"value":2693},"Suppose a consumer updates an order-search projection. It can record processed event IDs in its own database, atomically with its local change:",{"type":24,"tag":105,"props":2695,"children":2697},{"className":1622,"code":2696,"language":1624,"meta":7,"style":7},"CREATE TABLE processed_event (\n    consumer_name TEXT NOT NULL,\n    event_id UUID NOT NULL,\n    processed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    PRIMARY KEY (consumer_name, event_id)\n);\n",[2698],{"type":24,"tag":14,"props":2699,"children":2700},{"__ignoreMap":7},[2701,2721,2741,2756,2780,2793],{"type":24,"tag":114,"props":2702,"children":2703},{"class":116,"line":117},[2704,2708,2712,2717],{"type":24,"tag":114,"props":2705,"children":2706},{"style":1634},[2707],{"type":29,"value":1637},{"type":24,"tag":114,"props":2709,"children":2710},{"style":1634},[2711],{"type":29,"value":1642},{"type":24,"tag":114,"props":2713,"children":2714},{"style":234},[2715],{"type":29,"value":2716}," processed_event",{"type":24,"tag":114,"props":2718,"children":2719},{"style":1650},[2720],{"type":29,"value":1653},{"type":24,"tag":114,"props":2722,"children":2723},{"class":116,"line":126},[2724,2729,2733,2737],{"type":24,"tag":114,"props":2725,"children":2726},{"style":1650},[2727],{"type":29,"value":2728},"    consumer_name ",{"type":24,"tag":114,"props":2730,"children":2731},{"style":1634},[2732],{"type":29,"value":1684},{"type":24,"tag":114,"props":2734,"children":2735},{"style":1634},[2736],{"type":29,"value":1689},{"type":24,"tag":114,"props":2738,"children":2739},{"style":1650},[2740],{"type":29,"value":1671},{"type":24,"tag":114,"props":2742,"children":2743},{"class":116,"line":135},[2744,2748,2752],{"type":24,"tag":114,"props":2745,"children":2746},{"style":1650},[2747],{"type":29,"value":1661},{"type":24,"tag":114,"props":2749,"children":2750},{"style":1634},[2751],{"type":29,"value":1788},{"type":24,"tag":114,"props":2753,"children":2754},{"style":1650},[2755],{"type":29,"value":1671},{"type":24,"tag":114,"props":2757,"children":2758},{"class":116,"line":144},[2759,2764,2768,2772,2776],{"type":24,"tag":114,"props":2760,"children":2761},{"style":1650},[2762],{"type":29,"value":2763},"    processed_at ",{"type":24,"tag":114,"props":2765,"children":2766},{"style":1634},[2767],{"type":29,"value":1806},{"type":24,"tag":114,"props":2769,"children":2770},{"style":1634},[2771],{"type":29,"value":1689},{"type":24,"tag":114,"props":2773,"children":2774},{"style":1634},[2775],{"type":29,"value":1815},{"type":24,"tag":114,"props":2777,"children":2778},{"style":1650},[2779],{"type":29,"value":1820},{"type":24,"tag":114,"props":2781,"children":2782},{"class":116,"line":153},[2783,2788],{"type":24,"tag":114,"props":2784,"children":2785},{"style":1634},[2786],{"type":29,"value":2787},"    PRIMARY KEY",{"type":24,"tag":114,"props":2789,"children":2790},{"style":1650},[2791],{"type":29,"value":2792}," (consumer_name, event_id)\n",{"type":24,"tag":114,"props":2794,"children":2795},{"class":116,"line":162},[2796],{"type":24,"tag":114,"props":2797,"children":2798},{"style":1650},[2799],{"type":29,"value":1843},{"type":24,"tag":25,"props":2801,"children":2802},{},[2803,2805,2810,2812,2818,2820,2826,2828,2833],{"type":29,"value":2804},"For the ",{"type":24,"tag":14,"props":2806,"children":2808},{"className":2807},[],[2809],{"type":29,"value":1268},{"type":29,"value":2811}," event, assume ",{"type":24,"tag":14,"props":2813,"children":2815},{"className":2814},[],[2816],{"type":29,"value":2817},"order_projection",{"type":29,"value":2819}," contains an ",{"type":24,"tag":14,"props":2821,"children":2823},{"className":2822},[],[2824],{"type":29,"value":2825},"order_id",{"type":29,"value":2827}," primary key and a ",{"type":24,"tag":14,"props":2829,"children":2831},{"className":2830},[],[2832],{"type":29,"value":1938},{"type":29,"value":2834},". This statement gates the projection insert on whether the event is new to this consumer:",{"type":24,"tag":105,"props":2836,"children":2838},{"className":1622,"code":2837,"language":1624,"meta":7,"style":7},"BEGIN;\n\nWITH first_delivery AS (\n    INSERT INTO processed_event (consumer_name, event_id)\n    VALUES ('order-search', :event_id)\n    ON CONFLICT (consumer_name, event_id) DO NOTHING\n    RETURNING event_id\n)\nINSERT INTO order_projection (order_id, status)\nSELECT :order_id, 'CREATED'\nFROM first_delivery;\n\nCOMMIT;\n",[2839],{"type":24,"tag":14,"props":2840,"children":2841},{"__ignoreMap":7},[2842,2853,2860,2882,2895,2918,2930,2938,2945,2965,2982,2994,3001],{"type":24,"tag":114,"props":2843,"children":2844},{"class":116,"line":117},[2845,2849],{"type":24,"tag":114,"props":2846,"children":2847},{"style":1634},[2848],{"type":29,"value":1963},{"type":24,"tag":114,"props":2850,"children":2851},{"style":1650},[2852],{"type":29,"value":1912},{"type":24,"tag":114,"props":2854,"children":2855},{"class":116,"line":126},[2856],{"type":24,"tag":114,"props":2857,"children":2858},{"emptyLinePlaceholder":583},[2859],{"type":29,"value":586},{"type":24,"tag":114,"props":2861,"children":2862},{"class":116,"line":135},[2863,2868,2873,2878],{"type":24,"tag":114,"props":2864,"children":2865},{"style":1634},[2866],{"type":29,"value":2867},"WITH",{"type":24,"tag":114,"props":2869,"children":2870},{"style":1650},[2871],{"type":29,"value":2872}," first_delivery ",{"type":24,"tag":114,"props":2874,"children":2875},{"style":1634},[2876],{"type":29,"value":2877},"AS",{"type":24,"tag":114,"props":2879,"children":2880},{"style":1650},[2881],{"type":29,"value":1653},{"type":24,"tag":114,"props":2883,"children":2884},{"class":116,"line":144},[2885,2890],{"type":24,"tag":114,"props":2886,"children":2887},{"style":1634},[2888],{"type":29,"value":2889},"    INSERT INTO",{"type":24,"tag":114,"props":2891,"children":2892},{"style":1650},[2893],{"type":29,"value":2894}," processed_event (consumer_name, event_id)\n",{"type":24,"tag":114,"props":2896,"children":2897},{"class":116,"line":153},[2898,2903,2908,2913],{"type":24,"tag":114,"props":2899,"children":2900},{"style":1634},[2901],{"type":29,"value":2902},"    VALUES",{"type":24,"tag":114,"props":2904,"children":2905},{"style":1650},[2906],{"type":29,"value":2907}," (",{"type":24,"tag":114,"props":2909,"children":2910},{"style":246},[2911],{"type":29,"value":2912},"'order-search'",{"type":24,"tag":114,"props":2914,"children":2915},{"style":1650},[2916],{"type":29,"value":2917},", :event_id)\n",{"type":24,"tag":114,"props":2919,"children":2920},{"class":116,"line":162},[2921,2925],{"type":24,"tag":114,"props":2922,"children":2923},{"style":1634},[2924],{"type":29,"value":1878},{"type":24,"tag":114,"props":2926,"children":2927},{"style":1650},[2928],{"type":29,"value":2929}," CONFLICT (consumer_name, event_id) DO NOTHING\n",{"type":24,"tag":114,"props":2931,"children":2932},{"class":116,"line":609},[2933],{"type":24,"tag":114,"props":2934,"children":2935},{"style":1650},[2936],{"type":29,"value":2937},"    RETURNING event_id\n",{"type":24,"tag":114,"props":2939,"children":2940},{"class":116,"line":621},[2941],{"type":24,"tag":114,"props":2942,"children":2943},{"style":1650},[2944],{"type":29,"value":2005},{"type":24,"tag":114,"props":2946,"children":2947},{"class":116,"line":1795},[2948,2952,2957,2961],{"type":24,"tag":114,"props":2949,"children":2950},{"style":1634},[2951],{"type":29,"value":1982},{"type":24,"tag":114,"props":2953,"children":2954},{"style":1650},[2955],{"type":29,"value":2956}," order_projection (order_id, ",{"type":24,"tag":114,"props":2958,"children":2959},{"style":1634},[2960],{"type":29,"value":1938},{"type":24,"tag":114,"props":2962,"children":2963},{"style":1650},[2964],{"type":29,"value":2005},{"type":24,"tag":114,"props":2966,"children":2967},{"class":116,"line":1823},[2968,2972,2977],{"type":24,"tag":114,"props":2969,"children":2970},{"style":1634},[2971],{"type":29,"value":2318},{"type":24,"tag":114,"props":2973,"children":2974},{"style":1650},[2975],{"type":29,"value":2976}," :order_id, ",{"type":24,"tag":114,"props":2978,"children":2979},{"style":246},[2980],{"type":29,"value":2981},"'CREATED'\n",{"type":24,"tag":114,"props":2983,"children":2984},{"class":116,"line":1837},[2985,2989],{"type":24,"tag":114,"props":2986,"children":2987},{"style":1634},[2988],{"type":29,"value":2339},{"type":24,"tag":114,"props":2990,"children":2991},{"style":1650},[2992],{"type":29,"value":2993}," first_delivery;\n",{"type":24,"tag":114,"props":2995,"children":2996},{"class":116,"line":1846},[2997],{"type":24,"tag":114,"props":2998,"children":2999},{"emptyLinePlaceholder":583},[3000],{"type":29,"value":586},{"type":24,"tag":114,"props":3002,"children":3003},{"class":116,"line":1854},[3004,3008],{"type":24,"tag":114,"props":3005,"children":3006},{"style":1634},[3007],{"type":29,"value":2192},{"type":24,"tag":114,"props":3009,"children":3010},{"style":1650},[3011],{"type":29,"value":1912},{"type":24,"tag":25,"props":3013,"children":3014},{},[3015,3017,3024],{"type":29,"value":3016},"A duplicate event inserts no marker and therefore no projection row. If the projection write fails, the marker rolls back too. Acknowledge the broker message only after the transaction commits. If the consumer then crashes before acknowledging, redelivery is harmless for this local effect. This is an application of the ",{"type":24,"tag":94,"props":3018,"children":3021},{"href":3019,"rel":3020},"https://microservices.io/patterns/communication-style/idempotent-consumer.html",[98],[3022],{"type":29,"value":3023},"Idempotent Consumer pattern",{"type":29,"value":103},{"type":24,"tag":25,"props":3026,"children":3027},{},[3028],{"type":29,"value":3029},"A “have I seen this ID?” query followed by a separate transaction is not equivalent: two workers can both pass the check. The unique constraint and shared transaction are doing real work here.",{"type":24,"tag":25,"props":3031,"children":3032},{},[3033],{"type":29,"value":3034},"This protection stops at the local database boundary. Sending an email or calling another service inside the handler introduces another external effect. Use an idempotency mechanism supported by that destination, or record a new outgoing obligation in the consumer's own outbox with the corresponding recovery policy. Moving an unreliable dual write downstream does not solve it.",{"type":24,"tag":50,"props":3036,"children":3038},{"id":3037},"choose-polling-or-cdc-deliberately",[3039],{"type":29,"value":3040},"Choose polling or CDC deliberately",{"type":24,"tag":25,"props":3042,"children":3043},{},[3044],{"type":29,"value":3045},"Polling is straightforward to inspect: a worker queries pending rows and records publication progress. You own its scheduling, locking or leases, retries, backoff, and cleanup. Its latency and database load depend on the polling strategy.",{"type":24,"tag":25,"props":3047,"children":3048},{},[3049,3051,3058,3060,3065],{"type":29,"value":3050},"Change data capture is another way to implement the relay. Debezium's ",{"type":24,"tag":94,"props":3052,"children":3055},{"href":3053,"rel":3054},"https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html",[98],[3056],{"type":29,"value":3057},"Outbox Event Router",{"type":29,"value":3059}," can transform changes captured from an outbox table into broker messages. The application still writes a domain event in its transaction; CDC transports that record. Watching arbitrary business-table updates does not automatically provide a meaningful ",{"type":24,"tag":14,"props":3061,"children":3063},{"className":3062},[],[3064],{"type":29,"value":1268},{"type":29,"value":3066}," contract.",{"type":24,"tag":25,"props":3068,"children":3069},{},[3070],{"type":29,"value":3071},"The schema above is a custom polling schema, not Debezium's default table layout. A CDC implementation must map its columns, routing key, event identity, and payload to the connector configuration. Route outbox inserts deliberately and account for cleanup operations. Avoid accidentally treating publication-marker updates as new domain events, or running independent polling and CDC publishers for the same records without an explicit handover plan.",{"type":24,"tag":25,"props":3073,"children":3074},{},[3075,3077,3084],{"type":29,"value":3076},"CDC removes the application's polling loop but adds connector operations: offsets, snapshots, replication slots, retained WAL, restart behavior, and recovery after prolonged downtime. The ",{"type":24,"tag":94,"props":3078,"children":3081},{"href":3079,"rel":3080},"https://debezium.io/documentation/reference/stable/connectors/postgresql.html",[98],[3082],{"type":29,"value":3083},"PostgreSQL connector documentation",{"type":29,"value":3085}," describes those responsibilities. Do not assume changing the relay transport eliminates end-to-end duplicates or consumer idempotency requirements.",{"type":24,"tag":50,"props":3087,"children":3089},{"id":3088},"ordering-needs-a-separate-design",[3090],{"type":29,"value":3091},"Ordering needs a separate design",{"type":24,"tag":25,"props":3093,"children":3094},{},[3095,3100,3102,3108,3110,3116],{"type":24,"tag":14,"props":3096,"children":3098},{"className":3097},[],[3099],{"type":29,"value":1268},{"type":29,"value":3101}," followed by ",{"type":24,"tag":14,"props":3103,"children":3105},{"className":3104},[],[3106],{"type":29,"value":3107},"OrderCancelled",{"type":29,"value":3109}," may need to be applied in that order. The polling example does not guarantee it: a second worker can skip a locked earlier event and publish a later one first. Sorting by ",{"type":24,"tag":14,"props":3111,"children":3113},{"className":3112},[],[3114],{"type":29,"value":3115},"created_at",{"type":29,"value":3117}," or a generated ID does not establish commit order across concurrent transactions either.",{"type":24,"tag":25,"props":3119,"children":3120},{},[3121],{"type":29,"value":3122},"If the domain requires ordering per order, serialize the source transitions using an appropriate concurrency policy, and preserve that order through relay scheduling, broker routing, and consumer processing. For Kafka, using the aggregate ID as a partition key keeps that key together under a suitable partitioning strategy; it cannot repair events the relay already sent in the wrong order.",{"type":24,"tag":25,"props":3124,"children":3125},{},[3126],{"type":29,"value":3127},"An aggregate sequence can help consumers reject stale state or detect gaps, but define what it counts and which events a consumer is expected to see. A gap is not necessarily a lost event if the subscription intentionally filters some transitions.",{"type":24,"tag":25,"props":3129,"children":3130},{},[3131],{"type":29,"value":3132},"For a projection containing complete state snapshots, ignoring an older version may be acceptable. For a workflow that must execute every transition, skipping straight to the newest version can lose required work. Choose the policy from the business semantics, and test concurrent updates and retries against it.",{"type":24,"tag":50,"props":3134,"children":3136},{"id":3135},"operate-the-pending-work-as-part-of-the-product",[3137],{"type":29,"value":3138},"Operate the pending work as part of the product",{"type":24,"tag":25,"props":3140,"children":3141},{},[3142],{"type":29,"value":3143},"An outbox gives you a place to recover from; it does not run the recovery process for you. Monitor the oldest pending event's age, backlog size and growth, publication failures and retry counts, and the delay between committing state and consumers applying it. Broker lag alone misses unpublished outbox entries.",{"type":24,"tag":25,"props":3145,"children":3146},{},[3147],{"type":29,"value":3148},"Tie alerts to the workflow's acceptable delay. If an order can remain pending for a few seconds but not an hour, that expectation belongs in the service's operational objectives. Decide when a growing backlog should cause backpressure or make the command API stop accepting more work. The API can acknowledge a local commit while downstream work is pending, but must not imply that the entire distributed workflow has finished.",{"type":24,"tag":25,"props":3150,"children":3151},{},[3152],{"type":29,"value":3153},"Retry transient failures with bounded backoff and jitter. Repeatedly failing events need a visible quarantine or dead-letter process with ownership and a replay procedure. Moving an event aside does not mean its business obligation has been fulfilled. Where ordering matters, decide whether later events for that aggregate must wait.",{"type":24,"tag":25,"props":3155,"children":3156},{},[3157],{"type":29,"value":3158},"Retain unpublished records. Clean up published records according to an explicit recovery and replay policy, and coordinate CDC cleanup with connector recovery. The outbox is not automatically a permanent event store. Consumer deduplication records also need a retention horizon that covers possible redelivery and replay; deleting them too early allows old events to have effects again.",{"type":24,"tag":25,"props":3160,"children":3161},{},[3162],{"type":29,"value":3163},"Finally, reconcile critical business outcomes. Check, for example, whether created orders reach the expected downstream state within the allowed interval. Successful publication is only one step toward that outcome.",{"type":24,"tag":50,"props":3165,"children":3167},{"id":3166},"test-by-stopping-the-system-at-inconvenient-moments",[3168],{"type":29,"value":3169},"Test by stopping the system at inconvenient moments",{"type":24,"tag":25,"props":3171,"children":3172},{},[3173],{"type":29,"value":3174},"A happy-path test proves little about the failure windows that motivated this pattern. In an isolated environment, inject failures and check the durable result:",{"type":24,"tag":1186,"props":3176,"children":3177},{},[3178,3183,3188,3193,3198,3203],{"type":24,"tag":1149,"props":3179,"children":3180},{},[3181],{"type":29,"value":3182},"Fail the outbox insert and confirm that the business row does not commit.",{"type":24,"tag":1149,"props":3184,"children":3185},{},[3186],{"type":29,"value":3187},"Terminate the producer after its commit and confirm that the relay later publishes the pending event.",{"type":24,"tag":1149,"props":3189,"children":3190},{},[3191],{"type":29,"value":3192},"Let the broker accept an event, then stop the relay before it records success. Confirm redelivery uses the same ID and the consumer effect occurs once.",{"type":24,"tag":1149,"props":3194,"children":3195},{},[3196],{"type":29,"value":3197},"Stop the consumer after its database commit but before acknowledgement. Confirm the duplicate is recognized.",{"type":24,"tag":1149,"props":3199,"children":3200},{},[3201],{"type":29,"value":3202},"Keep the broker unavailable, observe the backlog and alert, then restore it and verify recovery without deleting pending work.",{"type":24,"tag":1149,"props":3204,"children":3205},{},[3206],{"type":29,"value":3207},"Exercise two transitions for the same aggregate with concurrent workers and a delayed first publication. Verify the ordering policy rather than assuming it.",{"type":24,"tag":25,"props":3209,"children":3210},{},[3211],{"type":29,"value":3212},"Also test poison messages, replay after deduplication cleanup, and connector recovery if using CDC. These tests should establish where your guarantees hold and what an operator must do when automatic recovery stops.",{"type":24,"tag":50,"props":3214,"children":3216},{"id":3215},"events-that-maintain-consistency-need-durable-publication",[3217],{"type":29,"value":3218},"Events that maintain consistency need durable publication",{"type":24,"tag":25,"props":3220,"children":3221},{},[3222],{"type":29,"value":3223},"Transactional Outbox is valuable when a committed business change must cause an event and the database and broker do not share an atomic transaction. Systems with different foundations, such as a durable event log as their source of truth, may solve that boundary differently. The requirement is reliable propagation, not a particular table name.",{"type":24,"tag":25,"props":3225,"children":3226},{},[3227],{"type":29,"value":3228},"For a system that relies on events to keep services coherent, the essential question is practical: if this process stops immediately after committing, where is the durable evidence of what still needs to happen?",{"type":24,"tag":25,"props":3230,"children":3231},{},[3232],{"type":29,"value":3233},"An outbox gives that question a concrete answer. Reliable relays, idempotent consumers, ordering rules, and operational recovery turn that answer into a system that can converge after failures.",{"type":24,"tag":50,"props":3235,"children":3236},{"id":1181},[3237],{"type":29,"value":1184},{"type":24,"tag":1186,"props":3239,"children":3240},{},[3241,3251,3261,3271,3281],{"type":24,"tag":1149,"props":3242,"children":3243},{},[3244,3250],{"type":24,"tag":94,"props":3245,"children":3247},{"href":1487,"rel":3246},[98],[3248],{"type":29,"value":3249},"Chris Richardson: Transactional Outbox",{"type":29,"value":103},{"type":24,"tag":1149,"props":3252,"children":3253},{},[3254,3260],{"type":24,"tag":94,"props":3255,"children":3257},{"href":3019,"rel":3256},[98],[3258],{"type":29,"value":3259},"Chris Richardson: Idempotent Consumer",{"type":29,"value":103},{"type":24,"tag":1149,"props":3262,"children":3263},{},[3264,3270],{"type":24,"tag":94,"props":3265,"children":3267},{"href":1608,"rel":3266},[98],[3268],{"type":29,"value":3269},"PostgreSQL: transactions",{"type":29,"value":103},{"type":24,"tag":1149,"props":3272,"children":3273},{},[3274,3280],{"type":24,"tag":94,"props":3275,"children":3277},{"href":3053,"rel":3276},[98],[3278],{"type":29,"value":3279},"Debezium: Outbox Event Router",{"type":29,"value":103},{"type":24,"tag":1149,"props":3282,"children":3283},{},[3284,3290],{"type":24,"tag":94,"props":3285,"children":3287},{"href":3079,"rel":3286},[98],[3288],{"type":29,"value":3289},"Debezium: PostgreSQL connector",{"type":29,"value":103},{"type":24,"tag":1222,"props":3292,"children":3293},{},[3294],{"type":29,"value":1226},{"title":7,"searchDepth":126,"depth":126,"links":3296},[3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308],{"id":1306,"depth":126,"text":1309},{"id":1496,"depth":126,"text":1499},{"id":1522,"depth":126,"text":1525},{"id":2272,"depth":126,"text":2275},{"id":2544,"depth":126,"text":2547},{"id":2685,"depth":126,"text":2688},{"id":3037,"depth":126,"text":3040},{"id":3088,"depth":126,"text":3091},{"id":3135,"depth":126,"text":3138},{"id":3166,"depth":126,"text":3169},{"id":3215,"depth":126,"text":3218},{"id":1181,"depth":126,"text":1184},"content:posts:transactional-outbox.md","posts/transactional-outbox.md","posts/transactional-outbox",{"_path":3313,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3314,"description":3315,"layout":10,"date":3316,"subtitle":3317,"image":3318,"optimized_image":3318,"category":14,"tags":3319,"author":19,"paginate":6,"body":3323,"_type":1237,"_id":5137,"_source":1239,"_file":5138,"_stem":5139,"_extension":1242},"/posts/api-evolution-strategy","Your API Needs an Evolution Strategy","A practical guide to evolving public APIs: compatibility, version selection, client migration, deprecation, contract testing, and safe rollback.","2025-05-15T11:00:00.000Z","Design changes around your clients and their ability to deploy independently","/assets/img/uploads/api-evolution-strategy.jpg",[14,3320,3321,1252,3322],"api-first","openapi","versioning",{"type":21,"children":3324,"toc":5126},[3325,3330,3335,3345,3350,3356,3361,3382,3396,3401,3407,3412,3417,3534,3547,3559,3564,3570,3575,3589,3594,3687,3709,3722,3727,3895,3931,3937,3950,4062,4067,4150,4155,4294,4307,4344,4349,4354,4360,4365,4398,4403,4491,4503,4508,4513,4518,4524,4529,4566,4571,4618,4671,4676,4681,4687,4692,4697,4784,4789,5018,5030,5044,5050,5055,5060,5065,5069,5122],{"type":24,"tag":25,"props":3326,"children":3327},{},[3328],{"type":29,"value":3329},"In my experience, teams used to building internal applications sometimes bring an assumption into API design: if an interface changes, we can coordinate with the people consuming it and deploy together. I have seen that assumption create substantial extra work around release planning and rollback. When several deployments depend on each other, a failure in one can leave the system in a combination nobody intended to run. Users can lose access to a service while teams work out how to recover it.",{"type":24,"tag":25,"props":3331,"children":3332},{},[3333],{"type":29,"value":3334},"I understand how that habit develops. When the consumer is another application in the same organization, its developers may be one conversation away. A coordinated release can feel like a reasonable shortcut. But public API clients have their own priorities, approval processes, maintenance windows, and users. Their release schedule does not belong to us.",{"type":24,"tag":25,"props":3336,"children":3337},{},[3338,3343],{"type":24,"tag":37,"props":3339,"children":3340},{},[3341],{"type":29,"value":3342},"If an API change requires your clients to deploy at the same time as you, treat that as a design warning.",{"type":29,"value":3344}," Before accepting the coordination cost, ask whether the contract could support a period in which both old and new clients work.",{"type":24,"tag":25,"props":3346,"children":3347},{},[3348],{"type":29,"value":3349},"Putting yourself in the client's place changes the design discussion. Can they keep serving their users while they migrate? Can they test the new behavior before switching? Can either side recover from a failed release independently? Those questions turn versioning into an engineering strategy.",{"type":24,"tag":50,"props":3351,"children":3353},{"id":3352},"start-with-the-contract-a-client-already-depends-on",[3354],{"type":29,"value":3355},"Start with the contract a client already depends on",{"type":24,"tag":25,"props":3357,"children":3358},{},[3359],{"type":29,"value":3360},"An API contract includes more than paths and JSON fields. Clients depend on validation rules, status codes, authorization requirements, default ordering, pagination, error responses, and the meaning of the values you return.",{"type":24,"tag":25,"props":3362,"children":3363},{},[3364,3366,3372,3374,3380],{"type":29,"value":3365},"A response can remain valid JSON while breaking an integration. Changing an amount from euros to cents preserves its numeric type but changes what it means. Replacing a synchronous ",{"type":24,"tag":14,"props":3367,"children":3369},{"className":3368},[],[3370],{"type":29,"value":3371},"201 Created",{"type":29,"value":3373}," workflow with ",{"type":24,"tag":14,"props":3375,"children":3377},{"className":3376},[],[3378],{"type":29,"value":3379},"202 Accepted",{"type":29,"value":3381}," asks the client to handle a different lifecycle. Making a search return only the first page when it previously returned all matches can silently lose data from an export.",{"type":24,"tag":25,"props":3383,"children":3384},{},[3385,3387,3394],{"type":29,"value":3386},"Compatibility therefore needs several kinds of review: can existing code communicate with the service, interpret the result correctly, and still perform its intended workflow? Google's ",{"type":24,"tag":94,"props":3388,"children":3391},{"href":3389,"rel":3390},"https://google.aip.dev/180",[98],[3392],{"type":29,"value":3393},"backwards compatibility guidance",{"type":29,"value":3395}," distinguishes wire, source, and semantic compatibility; it is a useful framework, even when your API follows a different style.",{"type":24,"tag":25,"props":3397,"children":3398},{},[3399],{"type":29,"value":3400},"Before designing a change, take one existing client journey and write it down. For an order search, that could be: authenticate, request a page, deserialize the response, display the orders, request the next page, and recover from an error. Review the change against that sequence, including the code the customer deployed months ago.",{"type":24,"tag":50,"props":3402,"children":3404},{"id":3403},"compatibility-has-a-direction",[3405],{"type":29,"value":3406},"Compatibility has a direction",{"type":24,"tag":25,"props":3408,"children":3409},{},[3410],{"type":29,"value":3411},"For an existing API version, the provider normally needs to keep accepting previously valid requests and returning responses that existing clients can handle. That does not automatically mean a new client can use new features against an old server. This distinction matters during rolling deployments and rollback.",{"type":24,"tag":25,"props":3413,"children":3414},{},[3415],{"type":29,"value":3416},"Consider these changes:",{"type":24,"tag":1349,"props":3418,"children":3419},{},[3420,3441],{"type":24,"tag":1353,"props":3421,"children":3422},{},[3423],{"type":24,"tag":1357,"props":3424,"children":3425},{},[3426,3431,3436],{"type":24,"tag":1361,"props":3427,"children":3428},{},[3429],{"type":29,"value":3430},"Proposed change",{"type":24,"tag":1361,"props":3432,"children":3433},{},[3434],{"type":29,"value":3435},"Effect on an existing client",{"type":24,"tag":1361,"props":3437,"children":3438},{},[3439],{"type":29,"value":3440},"Safer approach",{"type":24,"tag":1377,"props":3442,"children":3443},{},[3444,3462,3480,3498,3516],{"type":24,"tag":1357,"props":3445,"children":3446},{},[3447,3452,3457],{"type":24,"tag":1384,"props":3448,"children":3449},{},[3450],{"type":29,"value":3451},"Require a new request field",{"type":24,"tag":1384,"props":3453,"children":3454},{},[3455],{"type":29,"value":3456},"Requests that used to work can fail validation",{"type":24,"tag":1384,"props":3458,"children":3459},{},[3460],{"type":29,"value":3461},"Make it optional with a documented default that preserves the old behavior, or introduce a new contract",{"type":24,"tag":1357,"props":3463,"children":3464},{},[3465,3470,3475],{"type":24,"tag":1384,"props":3466,"children":3467},{},[3468],{"type":29,"value":3469},"Remove or rename a response field",{"type":24,"tag":1384,"props":3471,"children":3472},{},[3473],{"type":29,"value":3474},"Deserialization or application logic can fail",{"type":24,"tag":1384,"props":3476,"children":3477},{},[3478],{"type":29,"value":3479},"Retain the field during support, or offer the new representation in a separately selected version",{"type":24,"tag":1357,"props":3481,"children":3482},{},[3483,3488,3493],{"type":24,"tag":1384,"props":3484,"children":3485},{},[3486],{"type":29,"value":3487},"Add a response field",{"type":24,"tag":1384,"props":3489,"children":3490},{},[3491],{"type":29,"value":3492},"Often compatible, but strict decoders or schemas can reject it",{"type":24,"tag":1384,"props":3494,"children":3495},{},[3496],{"type":29,"value":3497},"Establish and test a policy for unknown response fields",{"type":24,"tag":1357,"props":3499,"children":3500},{},[3501,3506,3511],{"type":24,"tag":1384,"props":3502,"children":3503},{},[3504],{"type":29,"value":3505},"Return a new enum value",{"type":24,"tag":1384,"props":3507,"children":3508},{},[3509],{"type":29,"value":3510},"A generated enum parser or exhaustive branch can fail",{"type":24,"tag":1384,"props":3512,"children":3513},{},[3514],{"type":29,"value":3515},"Define extensible-enum behavior up front; otherwise gate the value behind an opt-in contract",{"type":24,"tag":1357,"props":3517,"children":3518},{},[3519,3524,3529],{"type":24,"tag":1384,"props":3520,"children":3521},{},[3522],{"type":29,"value":3523},"Change ordering, units, or error behavior",{"type":24,"tag":1384,"props":3525,"children":3526},{},[3527],{"type":29,"value":3528},"A client may parse the response and still behave incorrectly",{"type":24,"tag":1384,"props":3530,"children":3531},{},[3532],{"type":29,"value":3533},"Treat the behavioral change as part of compatibility review",{"type":24,"tag":25,"props":3535,"children":3536},{},[3537,3539,3545],{"type":29,"value":3538},"For example, adding an optional ",{"type":24,"tag":14,"props":3540,"children":3542},{"className":3541},[],[3543],{"type":29,"value":3544},"deliveryInstructions",{"type":29,"value":3546}," field to order creation can preserve old clients: omission must continue to produce the previous delivery behavior. The new server must support that omission deliberately. Requiring clients to start sending an empty string would still force a migration.",{"type":24,"tag":25,"props":3548,"children":3549},{},[3550,3552,3557],{"type":29,"value":3551},"Conversely, a new client that sends ",{"type":24,"tag":14,"props":3553,"children":3555},{"className":3554},[],[3556],{"type":29,"value":3544},{"type":29,"value":3558}," cannot assume an older server will accept or honor it. Deploy support before enabling its use, and decide what happens if the provider rolls back.",{"type":24,"tag":25,"props":3560,"children":3561},{},[3562],{"type":29,"value":3563},"Do not assume that every client ignores unknown response fields. If you publish an SDK, exercise its actual decoder. If your documented response schema prohibits additional properties, adding one can violate the contract you supplied. Define extension rules early and apply them consistently; changing the rules after clients have shipped does not update those clients.",{"type":24,"tag":50,"props":3565,"children":3567},{"id":3566},"separate-the-api-version-from-your-deployment-version",[3568],{"type":29,"value":3569},"Separate the API version from your deployment version",{"type":24,"tag":25,"props":3571,"children":3572},{},[3573],{"type":29,"value":3574},"A backend can have many releases while continuing to serve the same public contract. A database migration, a refactor, and a performance improvement do not each need a new client-facing API version.",{"type":24,"tag":25,"props":3576,"children":3577},{},[3578,3580,3587],{"type":29,"value":3579},"Use a new major contract when you need an incompatible change that cannot reasonably be delivered within the existing promises. Compatible additions can remain in the supported version. Google's ",{"type":24,"tag":94,"props":3581,"children":3584},{"href":3582,"rel":3583},"https://google.aip.dev/185",[98],[3585],{"type":29,"value":3586},"API versioning guidance",{"type":29,"value":3588}," describes this separation between a service's evolution and the versions consumers select.",{"type":24,"tag":25,"props":3590,"children":3591},{},[3592],{"type":29,"value":3593},"The selection mechanism should be explicit and predictable:",{"type":24,"tag":1349,"props":3595,"children":3596},{},[3597,3618],{"type":24,"tag":1353,"props":3598,"children":3599},{},[3600],{"type":24,"tag":1357,"props":3601,"children":3602},{},[3603,3608,3613],{"type":24,"tag":1361,"props":3604,"children":3605},{},[3606],{"type":29,"value":3607},"Mechanism",{"type":24,"tag":1361,"props":3609,"children":3610},{},[3611],{"type":29,"value":3612},"Example",{"type":24,"tag":1361,"props":3614,"children":3615},{},[3616],{"type":29,"value":3617},"Operational consideration",{"type":24,"tag":1377,"props":3619,"children":3620},{},[3621,3643,3665],{"type":24,"tag":1357,"props":3622,"children":3623},{},[3624,3629,3638],{"type":24,"tag":1384,"props":3625,"children":3626},{},[3627],{"type":29,"value":3628},"URL path",{"type":24,"tag":1384,"props":3630,"children":3631},{},[3632],{"type":24,"tag":14,"props":3633,"children":3635},{"className":3634},[],[3636],{"type":29,"value":3637},"/v2/orders",{"type":24,"tag":1384,"props":3639,"children":3640},{},[3641],{"type":29,"value":3642},"Visible in requests, routing, logs, and documentation",{"type":24,"tag":1357,"props":3644,"children":3645},{},[3646,3651,3660],{"type":24,"tag":1384,"props":3647,"children":3648},{},[3649],{"type":29,"value":3650},"Query parameter",{"type":24,"tag":1384,"props":3652,"children":3653},{},[3654],{"type":24,"tag":14,"props":3655,"children":3657},{"className":3656},[],[3658],{"type":29,"value":3659},"/orders?api-version=2025-05-15",{"type":24,"tag":1384,"props":3661,"children":3662},{},[3663],{"type":29,"value":3664},"Clients, gateways, and caches must preserve the parameter",{"type":24,"tag":1357,"props":3666,"children":3667},{},[3668,3673,3682],{"type":24,"tag":1384,"props":3669,"children":3670},{},[3671],{"type":29,"value":3672},"Request header or media type",{"type":24,"tag":1384,"props":3674,"children":3675},{},[3676],{"type":24,"tag":14,"props":3677,"children":3679},{"className":3678},[],[3680],{"type":29,"value":3681},"Accept: application/vnd.example.orders.v2+json",{"type":24,"tag":1384,"props":3683,"children":3684},{},[3685],{"type":29,"value":3686},"Gateways and representation caches must account for the selecting header",{"type":24,"tag":25,"props":3688,"children":3689},{},[3690,3692,3698,3700,3707],{"type":29,"value":3691},"I would use paths for the examples here because they make the two contracts easy to see. Other approaches work if routing, documentation, observability, and caching agree on the selection. With header-based representation selection, configure the cache key correctly and use the appropriate ",{"type":24,"tag":14,"props":3693,"children":3695},{"className":3694},[],[3696],{"type":29,"value":3697},"Vary",{"type":29,"value":3699}," response header; ",{"type":24,"tag":94,"props":3701,"children":3704},{"href":3702,"rel":3703},"https://www.rfc-editor.org/rfc/rfc9110.html#name-vary",[98],[3705],{"type":29,"value":3706},"HTTP semantics",{"type":29,"value":3708}," explains its role.",{"type":24,"tag":25,"props":3710,"children":3711},{},[3712,3714,3720],{"type":29,"value":3713},"Avoid silently moving an existing client to a newer contract because it omitted a version or requested a moving ",{"type":24,"tag":14,"props":3715,"children":3717},{"className":3716},[],[3718],{"type":29,"value":3719},"latest",{"type":29,"value":3721}," alias. Document the default, keep it stable for existing integrations, and reject unsupported explicit versions clearly.",{"type":24,"tag":25,"props":3723,"children":3724},{},[3725],{"type":29,"value":3726},"Also distinguish the version fields in an OpenAPI document:",{"type":24,"tag":105,"props":3728,"children":3732},{"className":3729,"code":3730,"language":3731,"meta":7,"style":7},"language-yaml shiki shiki-themes github-dark github-light","openapi: 3.1.0\ninfo:\n  title: Orders API\n  version: 2.0.0\npaths:\n  /v2/orders:\n    get:\n      summary: List orders using cursor pagination\n      responses:\n        '200':\n          description: An envelope containing items and the next cursor\n","yaml",[3733],{"type":24,"tag":14,"props":3734,"children":3735},{"__ignoreMap":7},[3736,3754,3767,3784,3801,3813,3825,3837,3854,3866,3878],{"type":24,"tag":114,"props":3737,"children":3738},{"class":116,"line":117},[3739,3744,3749],{"type":24,"tag":114,"props":3740,"children":3742},{"style":3741},"--shiki-default:#85E89D;--shiki-light:#22863A",[3743],{"type":29,"value":3321},{"type":24,"tag":114,"props":3745,"children":3746},{"style":1650},[3747],{"type":29,"value":3748},": ",{"type":24,"tag":114,"props":3750,"children":3751},{"style":240},[3752],{"type":29,"value":3753},"3.1.0\n",{"type":24,"tag":114,"props":3755,"children":3756},{"class":116,"line":126},[3757,3762],{"type":24,"tag":114,"props":3758,"children":3759},{"style":3741},[3760],{"type":29,"value":3761},"info",{"type":24,"tag":114,"props":3763,"children":3764},{"style":1650},[3765],{"type":29,"value":3766},":\n",{"type":24,"tag":114,"props":3768,"children":3769},{"class":116,"line":135},[3770,3775,3779],{"type":24,"tag":114,"props":3771,"children":3772},{"style":3741},[3773],{"type":29,"value":3774},"  title",{"type":24,"tag":114,"props":3776,"children":3777},{"style":1650},[3778],{"type":29,"value":3748},{"type":24,"tag":114,"props":3780,"children":3781},{"style":246},[3782],{"type":29,"value":3783},"Orders API\n",{"type":24,"tag":114,"props":3785,"children":3786},{"class":116,"line":144},[3787,3792,3796],{"type":24,"tag":114,"props":3788,"children":3789},{"style":3741},[3790],{"type":29,"value":3791},"  version",{"type":24,"tag":114,"props":3793,"children":3794},{"style":1650},[3795],{"type":29,"value":3748},{"type":24,"tag":114,"props":3797,"children":3798},{"style":240},[3799],{"type":29,"value":3800},"2.0.0\n",{"type":24,"tag":114,"props":3802,"children":3803},{"class":116,"line":153},[3804,3809],{"type":24,"tag":114,"props":3805,"children":3806},{"style":3741},[3807],{"type":29,"value":3808},"paths",{"type":24,"tag":114,"props":3810,"children":3811},{"style":1650},[3812],{"type":29,"value":3766},{"type":24,"tag":114,"props":3814,"children":3815},{"class":116,"line":162},[3816,3821],{"type":24,"tag":114,"props":3817,"children":3818},{"style":3741},[3819],{"type":29,"value":3820},"  /v2/orders",{"type":24,"tag":114,"props":3822,"children":3823},{"style":1650},[3824],{"type":29,"value":3766},{"type":24,"tag":114,"props":3826,"children":3827},{"class":116,"line":609},[3828,3833],{"type":24,"tag":114,"props":3829,"children":3830},{"style":3741},[3831],{"type":29,"value":3832},"    get",{"type":24,"tag":114,"props":3834,"children":3835},{"style":1650},[3836],{"type":29,"value":3766},{"type":24,"tag":114,"props":3838,"children":3839},{"class":116,"line":621},[3840,3845,3849],{"type":24,"tag":114,"props":3841,"children":3842},{"style":3741},[3843],{"type":29,"value":3844},"      summary",{"type":24,"tag":114,"props":3846,"children":3847},{"style":1650},[3848],{"type":29,"value":3748},{"type":24,"tag":114,"props":3850,"children":3851},{"style":246},[3852],{"type":29,"value":3853},"List orders using cursor pagination\n",{"type":24,"tag":114,"props":3855,"children":3856},{"class":116,"line":1795},[3857,3862],{"type":24,"tag":114,"props":3858,"children":3859},{"style":3741},[3860],{"type":29,"value":3861},"      responses",{"type":24,"tag":114,"props":3863,"children":3864},{"style":1650},[3865],{"type":29,"value":3766},{"type":24,"tag":114,"props":3867,"children":3868},{"class":116,"line":1823},[3869,3874],{"type":24,"tag":114,"props":3870,"children":3871},{"style":246},[3872],{"type":29,"value":3873},"        '200'",{"type":24,"tag":114,"props":3875,"children":3876},{"style":1650},[3877],{"type":29,"value":3766},{"type":24,"tag":114,"props":3879,"children":3880},{"class":116,"line":1837},[3881,3886,3890],{"type":24,"tag":114,"props":3882,"children":3883},{"style":3741},[3884],{"type":29,"value":3885},"          description",{"type":24,"tag":114,"props":3887,"children":3888},{"style":1650},[3889],{"type":29,"value":3748},{"type":24,"tag":114,"props":3891,"children":3892},{"style":246},[3893],{"type":29,"value":3894},"An envelope containing items and the next cursor\n",{"type":24,"tag":25,"props":3896,"children":3897},{},[3898,3900,3905,3907,3913,3915,3920,3922,3929],{"type":29,"value":3899},"This is a shortened document fragment. ",{"type":24,"tag":14,"props":3901,"children":3903},{"className":3902},[],[3904],{"type":29,"value":3321},{"type":29,"value":3906}," identifies the specification format; ",{"type":24,"tag":14,"props":3908,"children":3910},{"className":3909},[],[3911],{"type":29,"value":3912},"info.version",{"type":29,"value":3914}," versions the API document. Neither field implements request routing or changes a deployed client's behavior. The ",{"type":24,"tag":14,"props":3916,"children":3918},{"className":3917},[],[3919],{"type":29,"value":3637},{"type":29,"value":3921}," path needs an implementation. The ",{"type":24,"tag":94,"props":3923,"children":3926},{"href":3924,"rel":3925},"https://spec.openapis.org/oas/v3.1.0.html#info-object",[98],[3927],{"type":29,"value":3928},"OpenAPI Info Object definition",{"type":29,"value":3930}," makes the distinction explicit. An SDK's package version is another separate lifecycle: document which server contracts it supports.",{"type":24,"tag":50,"props":3932,"children":3934},{"id":3933},"a-small-response-change-that-breaks-a-real-client-shape",[3935],{"type":29,"value":3936},"A small response change that breaks a real client shape",{"type":24,"tag":25,"props":3938,"children":3939},{},[3940,3942,3948],{"type":29,"value":3941},"Suppose an existing endpoint supports offset pagination. A request to ",{"type":24,"tag":14,"props":3943,"children":3945},{"className":3944},[],[3946],{"type":29,"value":3947},"GET /v1/orders?limit=2&offset=0",{"type":29,"value":3949}," returns a JSON array:",{"type":24,"tag":105,"props":3951,"children":3955},{"className":3952,"code":3953,"language":3954,"meta":7,"style":7},"language-json shiki shiki-themes github-dark github-light","[\n  { \"id\": \"ord_102\", \"status\": \"CONFIRMED\" },\n  { \"id\": \"ord_101\", \"status\": \"PENDING\" }\n]\n","json",[3956],{"type":24,"tag":14,"props":3957,"children":3958},{"__ignoreMap":7},[3959,3967,4012,4054],{"type":24,"tag":114,"props":3960,"children":3961},{"class":116,"line":117},[3962],{"type":24,"tag":114,"props":3963,"children":3964},{"style":1650},[3965],{"type":29,"value":3966},"[\n",{"type":24,"tag":114,"props":3968,"children":3969},{"class":116,"line":126},[3970,3975,3980,3984,3989,3993,3998,4002,4007],{"type":24,"tag":114,"props":3971,"children":3972},{"style":1650},[3973],{"type":29,"value":3974},"  { ",{"type":24,"tag":114,"props":3976,"children":3977},{"style":240},[3978],{"type":29,"value":3979},"\"id\"",{"type":24,"tag":114,"props":3981,"children":3982},{"style":1650},[3983],{"type":29,"value":3748},{"type":24,"tag":114,"props":3985,"children":3986},{"style":246},[3987],{"type":29,"value":3988},"\"ord_102\"",{"type":24,"tag":114,"props":3990,"children":3991},{"style":1650},[3992],{"type":29,"value":1996},{"type":24,"tag":114,"props":3994,"children":3995},{"style":240},[3996],{"type":29,"value":3997},"\"status\"",{"type":24,"tag":114,"props":3999,"children":4000},{"style":1650},[4001],{"type":29,"value":3748},{"type":24,"tag":114,"props":4003,"children":4004},{"style":246},[4005],{"type":29,"value":4006},"\"CONFIRMED\"",{"type":24,"tag":114,"props":4008,"children":4009},{"style":1650},[4010],{"type":29,"value":4011}," },\n",{"type":24,"tag":114,"props":4013,"children":4014},{"class":116,"line":135},[4015,4019,4023,4027,4032,4036,4040,4044,4049],{"type":24,"tag":114,"props":4016,"children":4017},{"style":1650},[4018],{"type":29,"value":3974},{"type":24,"tag":114,"props":4020,"children":4021},{"style":240},[4022],{"type":29,"value":3979},{"type":24,"tag":114,"props":4024,"children":4025},{"style":1650},[4026],{"type":29,"value":3748},{"type":24,"tag":114,"props":4028,"children":4029},{"style":246},[4030],{"type":29,"value":4031},"\"ord_101\"",{"type":24,"tag":114,"props":4033,"children":4034},{"style":1650},[4035],{"type":29,"value":1996},{"type":24,"tag":114,"props":4037,"children":4038},{"style":240},[4039],{"type":29,"value":3997},{"type":24,"tag":114,"props":4041,"children":4042},{"style":1650},[4043],{"type":29,"value":3748},{"type":24,"tag":114,"props":4045,"children":4046},{"style":246},[4047],{"type":29,"value":4048},"\"PENDING\"",{"type":24,"tag":114,"props":4050,"children":4051},{"style":1650},[4052],{"type":29,"value":4053}," }\n",{"type":24,"tag":114,"props":4055,"children":4056},{"class":116,"line":144},[4057],{"type":24,"tag":114,"props":4058,"children":4059},{"style":1650},[4060],{"type":29,"value":4061},"]\n",{"type":24,"tag":25,"props":4063,"children":4064},{},[4065],{"type":29,"value":4066},"A client might contain this perfectly reasonable code:",{"type":24,"tag":105,"props":4068,"children":4072},{"className":4069,"code":4070,"language":4071,"meta":7,"style":7},"language-javascript shiki shiki-themes github-dark github-light","function orderIdsFromV1(body) {\n  return body.map(order => order.id);\n}\n","javascript",[4073],{"type":24,"tag":14,"props":4074,"children":4075},{"__ignoreMap":7},[4076,4105,4142],{"type":24,"tag":114,"props":4077,"children":4078},{"class":116,"line":117},[4079,4084,4089,4094,4100],{"type":24,"tag":114,"props":4080,"children":4081},{"style":1634},[4082],{"type":29,"value":4083},"function",{"type":24,"tag":114,"props":4085,"children":4086},{"style":234},[4087],{"type":29,"value":4088}," orderIdsFromV1",{"type":24,"tag":114,"props":4090,"children":4091},{"style":1650},[4092],{"type":29,"value":4093},"(",{"type":24,"tag":114,"props":4095,"children":4097},{"style":4096},"--shiki-default:#FFAB70;--shiki-light:#E36209",[4098],{"type":29,"value":4099},"body",{"type":24,"tag":114,"props":4101,"children":4102},{"style":1650},[4103],{"type":29,"value":4104},") {\n",{"type":24,"tag":114,"props":4106,"children":4107},{"class":116,"line":126},[4108,4113,4118,4123,4127,4132,4137],{"type":24,"tag":114,"props":4109,"children":4110},{"style":1634},[4111],{"type":29,"value":4112},"  return",{"type":24,"tag":114,"props":4114,"children":4115},{"style":1650},[4116],{"type":29,"value":4117}," body.",{"type":24,"tag":114,"props":4119,"children":4120},{"style":234},[4121],{"type":29,"value":4122},"map",{"type":24,"tag":114,"props":4124,"children":4125},{"style":1650},[4126],{"type":29,"value":4093},{"type":24,"tag":114,"props":4128,"children":4129},{"style":4096},[4130],{"type":29,"value":4131},"order",{"type":24,"tag":114,"props":4133,"children":4134},{"style":1634},[4135],{"type":29,"value":4136}," =>",{"type":24,"tag":114,"props":4138,"children":4139},{"style":1650},[4140],{"type":29,"value":4141}," order.id);\n",{"type":24,"tag":114,"props":4143,"children":4144},{"class":116,"line":135},[4145],{"type":24,"tag":114,"props":4146,"children":4147},{"style":1650},[4148],{"type":29,"value":4149},"}\n",{"type":24,"tag":25,"props":4151,"children":4152},{},[4153],{"type":29,"value":4154},"You want to introduce cursor pagination and a place for pagination metadata. The proposed response becomes:",{"type":24,"tag":105,"props":4156,"children":4158},{"className":3952,"code":4157,"language":3954,"meta":7,"style":7},"{\n  \"items\": [\n    { \"id\": \"ord_102\", \"status\": \"CONFIRMED\" },\n    { \"id\": \"ord_101\", \"status\": \"PENDING\" }\n  ],\n  \"nextCursor\": \"opaque-continuation-token\"\n}\n",[4159],{"type":24,"tag":14,"props":4160,"children":4161},{"__ignoreMap":7},[4162,4170,4183,4223,4262,4270,4287],{"type":24,"tag":114,"props":4163,"children":4164},{"class":116,"line":117},[4165],{"type":24,"tag":114,"props":4166,"children":4167},{"style":1650},[4168],{"type":29,"value":4169},"{\n",{"type":24,"tag":114,"props":4171,"children":4172},{"class":116,"line":126},[4173,4178],{"type":24,"tag":114,"props":4174,"children":4175},{"style":240},[4176],{"type":29,"value":4177},"  \"items\"",{"type":24,"tag":114,"props":4179,"children":4180},{"style":1650},[4181],{"type":29,"value":4182},": [\n",{"type":24,"tag":114,"props":4184,"children":4185},{"class":116,"line":135},[4186,4191,4195,4199,4203,4207,4211,4215,4219],{"type":24,"tag":114,"props":4187,"children":4188},{"style":1650},[4189],{"type":29,"value":4190},"    { ",{"type":24,"tag":114,"props":4192,"children":4193},{"style":240},[4194],{"type":29,"value":3979},{"type":24,"tag":114,"props":4196,"children":4197},{"style":1650},[4198],{"type":29,"value":3748},{"type":24,"tag":114,"props":4200,"children":4201},{"style":246},[4202],{"type":29,"value":3988},{"type":24,"tag":114,"props":4204,"children":4205},{"style":1650},[4206],{"type":29,"value":1996},{"type":24,"tag":114,"props":4208,"children":4209},{"style":240},[4210],{"type":29,"value":3997},{"type":24,"tag":114,"props":4212,"children":4213},{"style":1650},[4214],{"type":29,"value":3748},{"type":24,"tag":114,"props":4216,"children":4217},{"style":246},[4218],{"type":29,"value":4006},{"type":24,"tag":114,"props":4220,"children":4221},{"style":1650},[4222],{"type":29,"value":4011},{"type":24,"tag":114,"props":4224,"children":4225},{"class":116,"line":144},[4226,4230,4234,4238,4242,4246,4250,4254,4258],{"type":24,"tag":114,"props":4227,"children":4228},{"style":1650},[4229],{"type":29,"value":4190},{"type":24,"tag":114,"props":4231,"children":4232},{"style":240},[4233],{"type":29,"value":3979},{"type":24,"tag":114,"props":4235,"children":4236},{"style":1650},[4237],{"type":29,"value":3748},{"type":24,"tag":114,"props":4239,"children":4240},{"style":246},[4241],{"type":29,"value":4031},{"type":24,"tag":114,"props":4243,"children":4244},{"style":1650},[4245],{"type":29,"value":1996},{"type":24,"tag":114,"props":4247,"children":4248},{"style":240},[4249],{"type":29,"value":3997},{"type":24,"tag":114,"props":4251,"children":4252},{"style":1650},[4253],{"type":29,"value":3748},{"type":24,"tag":114,"props":4255,"children":4256},{"style":246},[4257],{"type":29,"value":4048},{"type":24,"tag":114,"props":4259,"children":4260},{"style":1650},[4261],{"type":29,"value":4053},{"type":24,"tag":114,"props":4263,"children":4264},{"class":116,"line":153},[4265],{"type":24,"tag":114,"props":4266,"children":4267},{"style":1650},[4268],{"type":29,"value":4269},"  ],\n",{"type":24,"tag":114,"props":4271,"children":4272},{"class":116,"line":162},[4273,4278,4282],{"type":24,"tag":114,"props":4274,"children":4275},{"style":240},[4276],{"type":29,"value":4277},"  \"nextCursor\"",{"type":24,"tag":114,"props":4279,"children":4280},{"style":1650},[4281],{"type":29,"value":3748},{"type":24,"tag":114,"props":4283,"children":4284},{"style":246},[4285],{"type":29,"value":4286},"\"opaque-continuation-token\"\n",{"type":24,"tag":114,"props":4288,"children":4289},{"class":116,"line":609},[4290],{"type":24,"tag":114,"props":4291,"children":4292},{"style":1650},[4293],{"type":29,"value":4149},{"type":24,"tag":25,"props":4295,"children":4296},{},[4297,4299,4305],{"type":29,"value":4298},"The old client now fails at ",{"type":24,"tag":14,"props":4300,"children":4302},{"className":4301},[],[4303],{"type":29,"value":4304},"body.map(...)",{"type":29,"value":4306},". Keeping every order field did not preserve compatibility: the response root changed from an array to an object.",{"type":24,"tag":25,"props":4308,"children":4309},{},[4310,4312,4318,4320,4326,4328,4334,4336,4342],{"type":29,"value":4311},"Offer that representation at ",{"type":24,"tag":14,"props":4313,"children":4315},{"className":4314},[],[4316],{"type":29,"value":4317},"GET /v2/orders?limit=2",{"type":29,"value":4319},", while ",{"type":24,"tag":14,"props":4321,"children":4323},{"className":4322},[],[4324],{"type":29,"value":4325},"/v1/orders",{"type":29,"value":4327}," keeps its existing shape and pagination behavior. A migrated client reads ",{"type":24,"tag":14,"props":4329,"children":4331},{"className":4330},[],[4332],{"type":29,"value":4333},"body.items",{"type":29,"value":4335}," and supplies the returned cursor on its next request. In this example, ",{"type":24,"tag":14,"props":4337,"children":4339},{"className":4338},[],[4340],{"type":29,"value":4341},"nextCursor: null",{"type":29,"value":4343}," means there is no next page; the token shown above is illustrative.",{"type":24,"tag":25,"props":4345,"children":4346},{},[4347],{"type":29,"value":4348},"Define the v2 pagination contract precisely: maximum and default page sizes, deterministic ordering with a unique tie-breaker, token opacity and expiry, which filters must remain unchanged, and what concurrent inserts or updates mean for traversal. Cursor pagination does not by itself promise a snapshot of the dataset. Clients need to know whether an export requires a separate snapshot mechanism.",{"type":24,"tag":25,"props":4350,"children":4351},{},[4352],{"type":29,"value":4353},"Use version-specific request and response adapters around shared application logic where practical. A new HTTP representation does not require copying the entire service. It does require preserving each supported contract's behavior, including its pagination rules. A v1 client should not start receiving cursor semantics simply because the internal query implementation changed.",{"type":24,"tag":50,"props":4355,"children":4357},{"id":4356},"deploy-support-first-migrate-clients-gradually",[4358],{"type":29,"value":4359},"Deploy support first, migrate clients gradually",{"type":24,"tag":25,"props":4361,"children":4362},{},[4363],{"type":29,"value":4364},"A migration should have valid intermediate states. For the order example, I would plan three provider stages:",{"type":24,"tag":1145,"props":4366,"children":4367},{},[4368,4378,4388],{"type":24,"tag":1149,"props":4369,"children":4370},{},[4371,4376],{"type":24,"tag":37,"props":4372,"children":4373},{},[4374],{"type":29,"value":4375},"Expand:",{"type":29,"value":4377}," deploy a release that serves both v1 and v2. Keep v1 working, and make v2 available in a test environment with documentation and examples. Complete the production rollout before inviting clients to depend on v2, or route v2 traffic only to instances that support it.",{"type":24,"tag":1149,"props":4379,"children":4380},{},[4381,4386],{"type":24,"tag":37,"props":4382,"children":4383},{},[4384],{"type":29,"value":4385},"Migrate:",{"type":29,"value":4387}," clients test and switch on their own schedules within the published support window. Observe traffic and failures by API version and authenticated integration identity. Maintain both contracts while the migration is active.",{"type":24,"tag":1149,"props":4389,"children":4390},{},[4391,4396],{"type":24,"tag":37,"props":4392,"children":4393},{},[4394],{"type":29,"value":4395},"Retire:",{"type":29,"value":4397}," remove v1 only after the announced policy and migration criteria have been met. Retirement is a separate operational decision from introducing v2.",{"type":24,"tag":25,"props":4399,"children":4400},{},[4401],{"type":29,"value":4402},"The compatibility matrix makes the deployment boundary visible:",{"type":24,"tag":1349,"props":4404,"children":4405},{},[4406,4437],{"type":24,"tag":1353,"props":4407,"children":4408},{},[4409],{"type":24,"tag":1357,"props":4410,"children":4411},{},[4412,4417,4422,4427,4432],{"type":24,"tag":1361,"props":4413,"children":4414},{},[4415],{"type":29,"value":4416},"Client behavior",{"type":24,"tag":1361,"props":4418,"children":4419},{},[4420],{"type":29,"value":4421},"Original server: v1 only",{"type":24,"tag":1361,"props":4423,"children":4424},{},[4425],{"type":29,"value":4426},"Compatibility release: v1 + v2",{"type":24,"tag":1361,"props":4428,"children":4429},{},[4430],{"type":29,"value":4431},"Later release: v1 + v2",{"type":24,"tag":1361,"props":4433,"children":4434},{},[4435],{"type":29,"value":4436},"After v1 retirement: v2 only",{"type":24,"tag":1377,"props":4438,"children":4439},{},[4440,4467],{"type":24,"tag":1357,"props":4441,"children":4442},{},[4443,4448,4453,4458,4462],{"type":24,"tag":1384,"props":4444,"children":4445},{},[4446],{"type":29,"value":4447},"Existing client uses v1",{"type":24,"tag":1384,"props":4449,"children":4450},{},[4451],{"type":29,"value":4452},"Works",{"type":24,"tag":1384,"props":4454,"children":4455},{},[4456],{"type":29,"value":4457},"Must work",{"type":24,"tag":1384,"props":4459,"children":4460},{},[4461],{"type":29,"value":4457},{"type":24,"tag":1384,"props":4463,"children":4464},{},[4465],{"type":29,"value":4466},"Unsupported",{"type":24,"tag":1357,"props":4468,"children":4469},{},[4470,4475,4479,4483,4487],{"type":24,"tag":1384,"props":4471,"children":4472},{},[4473],{"type":29,"value":4474},"Migrated client uses v2",{"type":24,"tag":1384,"props":4476,"children":4477},{},[4478],{"type":29,"value":4466},{"type":24,"tag":1384,"props":4480,"children":4481},{},[4482],{"type":29,"value":4457},{"type":24,"tag":1384,"props":4484,"children":4485},{},[4486],{"type":29,"value":4457},{"type":24,"tag":1384,"props":4488,"children":4489},{},[4490],{"type":29,"value":4452},{"type":24,"tag":25,"props":4492,"children":4493},{},[4494,4496,4501],{"type":29,"value":4495},"The second row explains why “we can always roll back to the previous version” is incomplete. ",{"type":24,"tag":37,"props":4497,"children":4498},{},[4499],{"type":29,"value":4500},"Once clients depend on v2, a server that only supports v1 is no longer a safe general rollback target.",{"type":29,"value":4502}," Keep a known-good release supporting both contracts, test recovery to that baseline, and retain the supporting infrastructure and data shape.",{"type":24,"tag":25,"props":4504,"children":4505},{},[4506],{"type":29,"value":4507},"If a new client deliberately supports falling back to v1, test that path. Do not assume fallback exists, and do not silently retry a state-changing request against another version: an ambiguous response may hide an operation that already succeeded. Recovery must preserve the operation's semantics and idempotency rules.",{"type":24,"tag":25,"props":4509,"children":4510},{},[4511],{"type":29,"value":4512},"A shared database can impose another rollback boundary. If an API release writes values that the older binary cannot interpret, preserving the HTTP route is insufficient. Keep schema and data changes compatible with the supported rollback baseline, backfill when needed, and defer destructive cleanup until the old readers and rollback window have been retired.",{"type":24,"tag":25,"props":4514,"children":4515},{},[4516],{"type":29,"value":4517},"Feature flags help control exposure, but disabling a feature that clients already require can still break them. Once a public capability is in use, recovery needs to respect the contract it established.",{"type":24,"tag":50,"props":4519,"children":4521},{"id":4520},"give-clients-a-migration-path-they-can-actually-follow",[4522],{"type":29,"value":4523},"Give clients a migration path they can actually follow",{"type":24,"tag":25,"props":4525,"children":4526},{},[4527],{"type":29,"value":4528},"A release note saying “v1 is deprecated; use v2” leaves most of the work with the consumer. Publish the request and response differences, concrete before-and-after examples, changed defaults and errors, SDK guidance, the support timeline, and a way to raise migration problems.",{"type":24,"tag":25,"props":4530,"children":4531},{},[4532,4534,4540,4542,4548,4550,4556,4558,4564],{"type":29,"value":4533},"For this example, show the actual client edits: read ",{"type":24,"tag":14,"props":4535,"children":4537},{"className":4536},[],[4538],{"type":29,"value":4539},"items",{"type":29,"value":4541},", stop sending ",{"type":24,"tag":14,"props":4543,"children":4545},{"className":4544},[],[4546],{"type":29,"value":4547},"offset",{"type":29,"value":4549},", persist the opaque cursor only for the intended traversal, and stop when ",{"type":24,"tag":14,"props":4551,"children":4553},{"className":4552},[],[4554],{"type":29,"value":4555},"nextCursor",{"type":29,"value":4557}," is ",{"type":24,"tag":14,"props":4559,"children":4561},{"className":4560},[],[4562],{"type":29,"value":4563},"null",{"type":29,"value":4565},". Explain the behavior for an expired cursor. Let clients try this against a representative test environment before switching production traffic.",{"type":24,"tag":25,"props":4567,"children":4568},{},[4569],{"type":29,"value":4570},"HTTP headers can make the lifecycle discoverable. Here is a fictional response announcing deprecation on 1 June 2025 and planned retirement on 1 December 2025:",{"type":24,"tag":105,"props":4572,"children":4574},{"className":107,"code":4573,"language":29,"meta":7,"style":7},"HTTP/1.1 200 OK\nContent-Type: application/json\nDeprecation: @1748736000\nSunset: Mon, 01 Dec 2025 00:00:00 GMT\nLink: \u003Chttps://api.example.com/docs/migrations/orders-v2>; rel=\"deprecation\"; type=\"text/html\"\n",[4575],{"type":24,"tag":14,"props":4576,"children":4577},{"__ignoreMap":7},[4578,4586,4594,4602,4610],{"type":24,"tag":114,"props":4579,"children":4580},{"class":116,"line":117},[4581],{"type":24,"tag":114,"props":4582,"children":4583},{},[4584],{"type":29,"value":4585},"HTTP/1.1 200 OK\n",{"type":24,"tag":114,"props":4587,"children":4588},{"class":116,"line":126},[4589],{"type":24,"tag":114,"props":4590,"children":4591},{},[4592],{"type":29,"value":4593},"Content-Type: application/json\n",{"type":24,"tag":114,"props":4595,"children":4596},{"class":116,"line":135},[4597],{"type":24,"tag":114,"props":4598,"children":4599},{},[4600],{"type":29,"value":4601},"Deprecation: @1748736000\n",{"type":24,"tag":114,"props":4603,"children":4604},{"class":116,"line":144},[4605],{"type":24,"tag":114,"props":4606,"children":4607},{},[4608],{"type":29,"value":4609},"Sunset: Mon, 01 Dec 2025 00:00:00 GMT\n",{"type":24,"tag":114,"props":4611,"children":4612},{"class":116,"line":153},[4613],{"type":24,"tag":114,"props":4614,"children":4615},{},[4616],{"type":29,"value":4617},"Link: \u003Chttps://api.example.com/docs/migrations/orders-v2>; rel=\"deprecation\"; type=\"text/html\"\n",{"type":24,"tag":25,"props":4619,"children":4620},{},[4621,4627,4629,4635,4637,4644,4646,4652,4654,4660,4662,4669],{"type":24,"tag":14,"props":4622,"children":4624},{"className":4623},[],[4625],{"type":29,"value":4626},"Deprecation",{"type":29,"value":4628}," uses a Structured Field date: ",{"type":24,"tag":14,"props":4630,"children":4632},{"className":4631},[],[4633],{"type":29,"value":4634},"@",{"type":29,"value":4636}," followed by Unix seconds. It is not a Boolean or an HTTP-date. ",{"type":24,"tag":94,"props":4638,"children":4641},{"href":4639,"rel":4640},"https://www.rfc-editor.org/rfc/rfc9745.html",[98],[4642],{"type":29,"value":4643},"RFC 9745",{"type":29,"value":4645}," defines that signal and the ",{"type":24,"tag":14,"props":4647,"children":4649},{"className":4648},[],[4650],{"type":29,"value":4651},"deprecation",{"type":29,"value":4653}," link relation. ",{"type":24,"tag":14,"props":4655,"children":4657},{"className":4656},[],[4658],{"type":29,"value":4659},"Sunset",{"type":29,"value":4661}," uses an HTTP-date and announces when the resource is expected to become unavailable; it does not prescribe the response after retirement. See ",{"type":24,"tag":94,"props":4663,"children":4666},{"href":4664,"rel":4665},"https://www.rfc-editor.org/rfc/rfc8594.html",[98],[4667],{"type":29,"value":4668},"RFC 8594",{"type":29,"value":4670},". Deprecation and shutdown are separate events; a deprecation notice alone is not a shutdown instruction.",{"type":24,"tag":25,"props":4672,"children":4673},{},[4674],{"type":29,"value":4675},"Headers complement communication. They do not guarantee a human has seen the notice. Use the channels clients agreed to receive, and track migration progress. A quiet week does not prove that an integration is unused: monthly jobs, seasonal workloads, and recovery tools may not appear in that window.",{"type":24,"tag":25,"props":4677,"children":4678},{},[4679],{"type":29,"value":4680},"Choose a support period that matches your clients' release constraints, then publish it and monitor it. The dates above illustrate the header formats, not a universal six-month policy. Supporting old versions has a cost, so make retirement deliberate rather than either indefinite by accident or abrupt for consumers.",{"type":24,"tag":50,"props":4682,"children":4684},{"id":4683},"test-the-combinations-you-intend-to-support",[4685],{"type":29,"value":4686},"Test the combinations you intend to support",{"type":24,"tag":25,"props":4688,"children":4689},{},[4690],{"type":29,"value":4691},"Keep the published contract as a baseline and review schema changes in CI. A structural diff can flag a removed field or a newly required input. It cannot prove that an unchanged numeric field still uses the same units, that authorization scopes remain sufficient, or that ordering and errors retain their meaning.",{"type":24,"tag":25,"props":4693,"children":4694},{},[4695],{"type":29,"value":4696},"For the migration above, preserve fixtures and tests that exercise these expectations:",{"type":24,"tag":1349,"props":4698,"children":4699},{},[4700,4716],{"type":24,"tag":1353,"props":4701,"children":4702},{},[4703],{"type":24,"tag":1357,"props":4704,"children":4705},{},[4706,4711],{"type":24,"tag":1361,"props":4707,"children":4708},{},[4709],{"type":29,"value":4710},"Check",{"type":24,"tag":1361,"props":4712,"children":4713},{},[4714],{"type":29,"value":4715},"What it protects",{"type":24,"tag":1377,"props":4717,"children":4718},{},[4719,4732,4745,4758,4771],{"type":24,"tag":1357,"props":4720,"children":4721},{},[4722,4727],{"type":24,"tag":1384,"props":4723,"children":4724},{},[4725],{"type":29,"value":4726},"Old requests against the new provider",{"type":24,"tag":1384,"props":4728,"children":4729},{},[4730],{"type":29,"value":4731},"Previously valid inputs and defaults still work",{"type":24,"tag":1357,"props":4733,"children":4734},{},[4735,4740],{"type":24,"tag":1384,"props":4736,"children":4737},{},[4738],{"type":29,"value":4739},"Old client decoder against new v1 responses",{"type":24,"tag":1384,"props":4741,"children":4742},{},[4743],{"type":29,"value":4744},"Existing consumers still understand the representation",{"type":24,"tag":1357,"props":4746,"children":4747},{},[4748,4753],{"type":24,"tag":1384,"props":4749,"children":4750},{},[4751],{"type":29,"value":4752},"v2 traversal over multiple pages",{"type":24,"tag":1384,"props":4754,"children":4755},{},[4756],{"type":29,"value":4757},"The envelope, cursor termination, ordering, and invalid-token behavior match the documented contract",{"type":24,"tag":1357,"props":4759,"children":4760},{},[4761,4766],{"type":24,"tag":1384,"props":4762,"children":4763},{},[4764],{"type":29,"value":4765},"Known-good rollback release against current data",{"type":24,"tag":1384,"props":4767,"children":4768},{},[4769],{"type":29,"value":4770},"Recovery remains possible after new writes and migrations",{"type":24,"tag":1357,"props":4772,"children":4773},{},[4774,4779],{"type":24,"tag":1384,"props":4775,"children":4776},{},[4777],{"type":29,"value":4778},"Both versions during the overlap period",{"type":24,"tag":1384,"props":4780,"children":4781},{},[4782],{"type":29,"value":4783},"A refactor shared by the adapters does not silently break one version",{"type":24,"tag":25,"props":4785,"children":4786},{},[4787],{"type":29,"value":4788},"A minimal JavaScript regression check can reuse the old decoder instead of replacing it with the new one. Assume the test server has a controlled fixture containing the two orders shown earlier:",{"type":24,"tag":105,"props":4790,"children":4792},{"className":4069,"code":4791,"language":4071,"meta":7,"style":7},"import assert from 'node:assert/strict';\n\nconst response = await fetch(\n  'http://localhost:8080/v1/orders?limit=2&offset=0'\n);\nassert.equal(response.status, 200);\n\nconst body = await response.json();\nassert.ok(Array.isArray(body));\nassert.deepEqual(orderIdsFromV1(body), ['ord_102', 'ord_101']);\n",[4793],{"type":24,"tag":14,"props":4794,"children":4795},{"__ignoreMap":7},[4796,4823,4830,4863,4871,4878,4905,4912,4946,4973],{"type":24,"tag":114,"props":4797,"children":4798},{"class":116,"line":117},[4799,4804,4809,4814,4819],{"type":24,"tag":114,"props":4800,"children":4801},{"style":1634},[4802],{"type":29,"value":4803},"import",{"type":24,"tag":114,"props":4805,"children":4806},{"style":1650},[4807],{"type":29,"value":4808}," assert ",{"type":24,"tag":114,"props":4810,"children":4811},{"style":1634},[4812],{"type":29,"value":4813},"from",{"type":24,"tag":114,"props":4815,"children":4816},{"style":246},[4817],{"type":29,"value":4818}," 'node:assert/strict'",{"type":24,"tag":114,"props":4820,"children":4821},{"style":1650},[4822],{"type":29,"value":1912},{"type":24,"tag":114,"props":4824,"children":4825},{"class":116,"line":126},[4826],{"type":24,"tag":114,"props":4827,"children":4828},{"emptyLinePlaceholder":583},[4829],{"type":29,"value":586},{"type":24,"tag":114,"props":4831,"children":4832},{"class":116,"line":135},[4833,4838,4843,4848,4853,4858],{"type":24,"tag":114,"props":4834,"children":4835},{"style":1634},[4836],{"type":29,"value":4837},"const",{"type":24,"tag":114,"props":4839,"children":4840},{"style":240},[4841],{"type":29,"value":4842}," response",{"type":24,"tag":114,"props":4844,"children":4845},{"style":1634},[4846],{"type":29,"value":4847}," =",{"type":24,"tag":114,"props":4849,"children":4850},{"style":1634},[4851],{"type":29,"value":4852}," await",{"type":24,"tag":114,"props":4854,"children":4855},{"style":234},[4856],{"type":29,"value":4857}," fetch",{"type":24,"tag":114,"props":4859,"children":4860},{"style":1650},[4861],{"type":29,"value":4862},"(\n",{"type":24,"tag":114,"props":4864,"children":4865},{"class":116,"line":144},[4866],{"type":24,"tag":114,"props":4867,"children":4868},{"style":246},[4869],{"type":29,"value":4870},"  'http://localhost:8080/v1/orders?limit=2&offset=0'\n",{"type":24,"tag":114,"props":4872,"children":4873},{"class":116,"line":153},[4874],{"type":24,"tag":114,"props":4875,"children":4876},{"style":1650},[4877],{"type":29,"value":1843},{"type":24,"tag":114,"props":4879,"children":4880},{"class":116,"line":162},[4881,4886,4891,4896,4901],{"type":24,"tag":114,"props":4882,"children":4883},{"style":1650},[4884],{"type":29,"value":4885},"assert.",{"type":24,"tag":114,"props":4887,"children":4888},{"style":234},[4889],{"type":29,"value":4890},"equal",{"type":24,"tag":114,"props":4892,"children":4893},{"style":1650},[4894],{"type":29,"value":4895},"(response.status, ",{"type":24,"tag":114,"props":4897,"children":4898},{"style":240},[4899],{"type":29,"value":4900},"200",{"type":24,"tag":114,"props":4902,"children":4903},{"style":1650},[4904],{"type":29,"value":1843},{"type":24,"tag":114,"props":4906,"children":4907},{"class":116,"line":609},[4908],{"type":24,"tag":114,"props":4909,"children":4910},{"emptyLinePlaceholder":583},[4911],{"type":29,"value":586},{"type":24,"tag":114,"props":4913,"children":4914},{"class":116,"line":621},[4915,4919,4924,4928,4932,4937,4941],{"type":24,"tag":114,"props":4916,"children":4917},{"style":1634},[4918],{"type":29,"value":4837},{"type":24,"tag":114,"props":4920,"children":4921},{"style":240},[4922],{"type":29,"value":4923}," body",{"type":24,"tag":114,"props":4925,"children":4926},{"style":1634},[4927],{"type":29,"value":4847},{"type":24,"tag":114,"props":4929,"children":4930},{"style":1634},[4931],{"type":29,"value":4852},{"type":24,"tag":114,"props":4933,"children":4934},{"style":1650},[4935],{"type":29,"value":4936}," response.",{"type":24,"tag":114,"props":4938,"children":4939},{"style":234},[4940],{"type":29,"value":3954},{"type":24,"tag":114,"props":4942,"children":4943},{"style":1650},[4944],{"type":29,"value":4945},"();\n",{"type":24,"tag":114,"props":4947,"children":4948},{"class":116,"line":1795},[4949,4953,4958,4963,4968],{"type":24,"tag":114,"props":4950,"children":4951},{"style":1650},[4952],{"type":29,"value":4885},{"type":24,"tag":114,"props":4954,"children":4955},{"style":234},[4956],{"type":29,"value":4957},"ok",{"type":24,"tag":114,"props":4959,"children":4960},{"style":1650},[4961],{"type":29,"value":4962},"(Array.",{"type":24,"tag":114,"props":4964,"children":4965},{"style":234},[4966],{"type":29,"value":4967},"isArray",{"type":24,"tag":114,"props":4969,"children":4970},{"style":1650},[4971],{"type":29,"value":4972},"(body));\n",{"type":24,"tag":114,"props":4974,"children":4975},{"class":116,"line":1823},[4976,4980,4985,4989,4994,4999,5004,5008,5013],{"type":24,"tag":114,"props":4977,"children":4978},{"style":1650},[4979],{"type":29,"value":4885},{"type":24,"tag":114,"props":4981,"children":4982},{"style":234},[4983],{"type":29,"value":4984},"deepEqual",{"type":24,"tag":114,"props":4986,"children":4987},{"style":1650},[4988],{"type":29,"value":4093},{"type":24,"tag":114,"props":4990,"children":4991},{"style":234},[4992],{"type":29,"value":4993},"orderIdsFromV1",{"type":24,"tag":114,"props":4995,"children":4996},{"style":1650},[4997],{"type":29,"value":4998},"(body), [",{"type":24,"tag":114,"props":5000,"children":5001},{"style":246},[5002],{"type":29,"value":5003},"'ord_102'",{"type":24,"tag":114,"props":5005,"children":5006},{"style":1650},[5007],{"type":29,"value":1996},{"type":24,"tag":114,"props":5009,"children":5010},{"style":246},[5011],{"type":29,"value":5012},"'ord_101'",{"type":24,"tag":114,"props":5014,"children":5015},{"style":1650},[5016],{"type":29,"value":5017},"]);\n",{"type":24,"tag":25,"props":5019,"children":5020},{},[5021,5023,5028],{"type":29,"value":5022},"Here ",{"type":24,"tag":14,"props":5024,"children":5026},{"className":5025},[],[5027],{"type":29,"value":4993},{"type":29,"value":5029}," is the unchanged function shown above. Run the check against the candidate provider with deterministic test data and the authentication required by your API. It catches the envelope change; broader tests must cover validation, error handling, pagination, and semantics. Do not update the old compatibility fixture just to make an incompatible release pass.",{"type":24,"tag":25,"props":5031,"children":5032},{},[5033,5035,5042],{"type":29,"value":5034},"Consumer-driven contract testing can add evidence from known integrations. Consumers exercise their real client code and publish expected interactions; the provider verifies those interactions against its implementation. ",{"type":24,"tag":94,"props":5036,"children":5039},{"href":5037,"rel":5038},"https://docs.pact.io/getting_started/how_pact_works",[98],[5040],{"type":29,"value":5041},"Pact's workflow",{"type":29,"value":5043}," explains this approach. For a public API, those contracts cover participating consumers, not every client in the world. They support your documented compatibility policy; they do not replace it.",{"type":24,"tag":50,"props":5045,"children":5047},{"id":5046},"treat-independent-deployment-as-a-design-outcome",[5048],{"type":29,"value":5049},"Treat independent deployment as a design outcome",{"type":24,"tag":25,"props":5051,"children":5052},{},[5053],{"type":29,"value":5054},"Teams still need to communicate about API changes. The goal is to give that communication a migration window instead of making it a requirement that everyone presses deploy together.",{"type":24,"tag":25,"props":5056,"children":5057},{},[5058],{"type":29,"value":5059},"Before releasing a change, ask what happens if a client does nothing, if it upgrades next month, or if your new release has to be rolled back. If the only safe answer is a synchronized deployment, revisit the contract and the intermediate states.",{"type":24,"tag":25,"props":5061,"children":5062},{},[5063],{"type":29,"value":5064},"Thinking from the client's side makes those states easier to design. It also makes the API easier to trust: users can keep doing their work while the teams on either side improve the software at their own pace.",{"type":24,"tag":50,"props":5066,"children":5067},{"id":1181},[5068],{"type":29,"value":1184},{"type":24,"tag":1186,"props":5070,"children":5071},{},[5072,5082,5092,5102,5112],{"type":24,"tag":1149,"props":5073,"children":5074},{},[5075,5081],{"type":24,"tag":94,"props":5076,"children":5078},{"href":3389,"rel":5077},[98],[5079],{"type":29,"value":5080},"Google AIP-180: backwards compatibility",{"type":29,"value":103},{"type":24,"tag":1149,"props":5083,"children":5084},{},[5085,5091],{"type":24,"tag":94,"props":5086,"children":5088},{"href":3582,"rel":5087},[98],[5089],{"type":29,"value":5090},"Google AIP-185: API versioning",{"type":29,"value":103},{"type":24,"tag":1149,"props":5093,"children":5094},{},[5095,5101],{"type":24,"tag":94,"props":5096,"children":5098},{"href":4639,"rel":5097},[98],[5099],{"type":29,"value":5100},"RFC 9745: the Deprecation HTTP response header",{"type":29,"value":103},{"type":24,"tag":1149,"props":5103,"children":5104},{},[5105,5111],{"type":24,"tag":94,"props":5106,"children":5108},{"href":4664,"rel":5107},[98],[5109],{"type":29,"value":5110},"RFC 8594: the Sunset HTTP header",{"type":29,"value":103},{"type":24,"tag":1149,"props":5113,"children":5114},{},[5115,5121],{"type":24,"tag":94,"props":5116,"children":5118},{"href":5037,"rel":5117},[98],[5119],{"type":29,"value":5120},"Pact: how consumer and provider contract tests work",{"type":29,"value":103},{"type":24,"tag":1222,"props":5123,"children":5124},{},[5125],{"type":29,"value":1226},{"title":7,"searchDepth":126,"depth":126,"links":5127},[5128,5129,5130,5131,5132,5133,5134,5135,5136],{"id":3352,"depth":126,"text":3355},{"id":3403,"depth":126,"text":3406},{"id":3566,"depth":126,"text":3569},{"id":3933,"depth":126,"text":3936},{"id":4356,"depth":126,"text":4359},{"id":4520,"depth":126,"text":4523},{"id":4683,"depth":126,"text":4686},{"id":5046,"depth":126,"text":5049},{"id":1181,"depth":126,"text":1184},"content:posts:api-evolution-strategy.md","posts/api-evolution-strategy.md","posts/api-evolution-strategy",{"_path":5141,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":5142,"description":5143,"layout":10,"date":5144,"subtitle":5145,"image":5146,"optimized_image":5146,"category":14,"tags":5147,"author":19,"paginate":6,"body":5151,"_type":1237,"_id":8104,"_source":1239,"_file":8105,"_stem":8106,"_extension":1242},"/posts/n-plus-one-queries","The N+1 Query Problem: Finding and Fixing a Slow Search Endpoint","Understand the N+1 query problem, detect it in a Java service backed by PostgreSQL, and compare projections, fetch joins, batching, and pagination strategies.","2025-04-15T11:00:00.000Z","Follow the SQL behind a Java endpoint before choosing a fix","/assets/img/uploads/n-plus-one-queries.jpg",[14,5148,1254,5149,5150],"java","performance","database",{"type":21,"children":5152,"toc":8086},[5153,5158,5163,5168,5178,5183,5189,5194,5205,5210,5468,5517,5644,5649,5927,5939,5952,5957,6216,6221,6226,6232,6237,6242,6247,6257,6263,6270,6275,6280,6376,6389,6395,6400,6520,6549,6554,6559,6565,6578,6713,6727,6732,6791,6811,6817,6822,6828,6833,6881,6886,7019,7024,7038,7044,7049,7174,7195,7200,7277,7290,7303,7309,7314,7382,7387,7408,7413,7419,7424,7437,7442,7521,7534,7539,7969,7974,7987,7992,7998,8003,8008,8013,8018,8024,8029,8034,8038,8082],{"type":24,"tag":25,"props":5154,"children":5155},{},[5156],{"type":29,"value":5157},"While talking with a teammate about a project I work on, already more than three years old, we started looking at a search endpoint in a Java service backed by PostgreSQL. Its performance was poor, with worrying spikes in average response time. An ordinary search was taking far longer than we would expect from it.",{"type":24,"tag":25,"props":5159,"children":5160},{},[5161],{"type":29,"value":5162},"That conversation stayed with me because these are the kinds of problems that can sit inside familiar code. A service has been around for years, the endpoint returns the right data, and everyone has other work to do. Familiarity can make it easy to stop asking how much work a request actually performs.",{"type":24,"tag":25,"props":5164,"children":5165},{},[5166],{"type":29,"value":5167},"The N+1 query problem is one of the fundamentals every developer working with persisted data should understand: how it happens, how to recognize it, and how to choose a fix. That includes being willing to revisit our own code. Experience should make us more curious about these problems, and more helpful when we find them together.",{"type":24,"tag":25,"props":5169,"children":5170},{},[5171,5176],{"type":24,"tag":37,"props":5172,"children":5173},{},[5174],{"type":29,"value":5175},"Your service's response times represent you and your team.",{"type":29,"value":5177}," They are part of what people experience when they use something you have built. We cannot prevent every performance issue, but we can make a habit of investigating them, sharing what we learn, and leaving the service better than we found it.",{"type":24,"tag":25,"props":5179,"children":5180},{},[5181],{"type":29,"value":5182},"Let's make that practical. The order-search example below is illustrative, not a reconstruction of that project's code or measurements. It uses Spring Data JPA and Hibernate 6 to make the database interactions visible.",{"type":24,"tag":50,"props":5184,"children":5186},{"id":5185},"what-n1-actually-means",[5187],{"type":29,"value":5188},"What N+1 actually means",{"type":24,"tag":25,"props":5190,"children":5191},{},[5192],{"type":29,"value":5193},"Imagine an endpoint that returns a page of orders, including each customer's name. The application first queries the orders. Then, while building the response, it retrieves a customer separately for each order.",{"type":24,"tag":25,"props":5195,"children":5196},{},[5197,5199,5204],{"type":29,"value":5198},"That is one initial query, followed by N additional queries for N results: ",{"type":24,"tag":37,"props":5200,"children":5201},{},[5202],{"type":29,"value":5203},"1 + N",{"type":29,"value":103},{"type":24,"tag":25,"props":5206,"children":5207},{},[5208],{"type":29,"value":5209},"Here is the relevant part of an entity mapping. Imports, constructors, and accessors are omitted:",{"type":24,"tag":105,"props":5211,"children":5214},{"className":5212,"code":5213,"language":5148,"meta":7,"style":7},"language-java shiki shiki-themes github-dark github-light","@Entity\n@Table(name = \"purchase_order\")\npublic class PurchaseOrder {\n    @Id\n    private Long id;\n\n    private String status;\n    private Instant createdAt;\n\n    @ManyToOne(fetch = FetchType.LAZY, optional = false)\n    @JoinColumn(name = \"customer_id\", nullable = false)\n    private Customer customer;\n}\n",[5215],{"type":24,"tag":14,"props":5216,"children":5217},{"__ignoreMap":7},[5218,5230,5264,5287,5300,5313,5320,5332,5344,5351,5399,5449,5461],{"type":24,"tag":114,"props":5219,"children":5220},{"class":116,"line":117},[5221,5225],{"type":24,"tag":114,"props":5222,"children":5223},{"style":1650},[5224],{"type":29,"value":4634},{"type":24,"tag":114,"props":5226,"children":5227},{"style":1634},[5228],{"type":29,"value":5229},"Entity\n",{"type":24,"tag":114,"props":5231,"children":5232},{"class":116,"line":126},[5233,5237,5242,5246,5251,5255,5260],{"type":24,"tag":114,"props":5234,"children":5235},{"style":1650},[5236],{"type":29,"value":4634},{"type":24,"tag":114,"props":5238,"children":5239},{"style":1634},[5240],{"type":29,"value":5241},"Table",{"type":24,"tag":114,"props":5243,"children":5244},{"style":1650},[5245],{"type":29,"value":4093},{"type":24,"tag":114,"props":5247,"children":5248},{"style":240},[5249],{"type":29,"value":5250},"name",{"type":24,"tag":114,"props":5252,"children":5253},{"style":1634},[5254],{"type":29,"value":4847},{"type":24,"tag":114,"props":5256,"children":5257},{"style":246},[5258],{"type":29,"value":5259}," \"purchase_order\"",{"type":24,"tag":114,"props":5261,"children":5262},{"style":1650},[5263],{"type":29,"value":2005},{"type":24,"tag":114,"props":5265,"children":5266},{"class":116,"line":135},[5267,5272,5277,5282],{"type":24,"tag":114,"props":5268,"children":5269},{"style":1634},[5270],{"type":29,"value":5271},"public",{"type":24,"tag":114,"props":5273,"children":5274},{"style":1634},[5275],{"type":29,"value":5276}," class",{"type":24,"tag":114,"props":5278,"children":5279},{"style":234},[5280],{"type":29,"value":5281}," PurchaseOrder",{"type":24,"tag":114,"props":5283,"children":5284},{"style":1650},[5285],{"type":29,"value":5286}," {\n",{"type":24,"tag":114,"props":5288,"children":5289},{"class":116,"line":144},[5290,5295],{"type":24,"tag":114,"props":5291,"children":5292},{"style":1650},[5293],{"type":29,"value":5294},"    @",{"type":24,"tag":114,"props":5296,"children":5297},{"style":1634},[5298],{"type":29,"value":5299},"Id\n",{"type":24,"tag":114,"props":5301,"children":5302},{"class":116,"line":153},[5303,5308],{"type":24,"tag":114,"props":5304,"children":5305},{"style":1634},[5306],{"type":29,"value":5307},"    private",{"type":24,"tag":114,"props":5309,"children":5310},{"style":1650},[5311],{"type":29,"value":5312}," Long id;\n",{"type":24,"tag":114,"props":5314,"children":5315},{"class":116,"line":162},[5316],{"type":24,"tag":114,"props":5317,"children":5318},{"emptyLinePlaceholder":583},[5319],{"type":29,"value":586},{"type":24,"tag":114,"props":5321,"children":5322},{"class":116,"line":609},[5323,5327],{"type":24,"tag":114,"props":5324,"children":5325},{"style":1634},[5326],{"type":29,"value":5307},{"type":24,"tag":114,"props":5328,"children":5329},{"style":1650},[5330],{"type":29,"value":5331}," String status;\n",{"type":24,"tag":114,"props":5333,"children":5334},{"class":116,"line":621},[5335,5339],{"type":24,"tag":114,"props":5336,"children":5337},{"style":1634},[5338],{"type":29,"value":5307},{"type":24,"tag":114,"props":5340,"children":5341},{"style":1650},[5342],{"type":29,"value":5343}," Instant createdAt;\n",{"type":24,"tag":114,"props":5345,"children":5346},{"class":116,"line":1795},[5347],{"type":24,"tag":114,"props":5348,"children":5349},{"emptyLinePlaceholder":583},[5350],{"type":29,"value":586},{"type":24,"tag":114,"props":5352,"children":5353},{"class":116,"line":1823},[5354,5358,5363,5367,5372,5376,5381,5386,5390,5395],{"type":24,"tag":114,"props":5355,"children":5356},{"style":1650},[5357],{"type":29,"value":5294},{"type":24,"tag":114,"props":5359,"children":5360},{"style":1634},[5361],{"type":29,"value":5362},"ManyToOne",{"type":24,"tag":114,"props":5364,"children":5365},{"style":1650},[5366],{"type":29,"value":4093},{"type":24,"tag":114,"props":5368,"children":5369},{"style":240},[5370],{"type":29,"value":5371},"fetch",{"type":24,"tag":114,"props":5373,"children":5374},{"style":1634},[5375],{"type":29,"value":4847},{"type":24,"tag":114,"props":5377,"children":5378},{"style":1650},[5379],{"type":29,"value":5380}," FetchType.LAZY, ",{"type":24,"tag":114,"props":5382,"children":5383},{"style":240},[5384],{"type":29,"value":5385},"optional",{"type":24,"tag":114,"props":5387,"children":5388},{"style":1634},[5389],{"type":29,"value":4847},{"type":24,"tag":114,"props":5391,"children":5392},{"style":240},[5393],{"type":29,"value":5394}," false",{"type":24,"tag":114,"props":5396,"children":5397},{"style":1650},[5398],{"type":29,"value":2005},{"type":24,"tag":114,"props":5400,"children":5401},{"class":116,"line":1837},[5402,5406,5411,5415,5419,5423,5428,5432,5437,5441,5445],{"type":24,"tag":114,"props":5403,"children":5404},{"style":1650},[5405],{"type":29,"value":5294},{"type":24,"tag":114,"props":5407,"children":5408},{"style":1634},[5409],{"type":29,"value":5410},"JoinColumn",{"type":24,"tag":114,"props":5412,"children":5413},{"style":1650},[5414],{"type":29,"value":4093},{"type":24,"tag":114,"props":5416,"children":5417},{"style":240},[5418],{"type":29,"value":5250},{"type":24,"tag":114,"props":5420,"children":5421},{"style":1634},[5422],{"type":29,"value":4847},{"type":24,"tag":114,"props":5424,"children":5425},{"style":246},[5426],{"type":29,"value":5427}," \"customer_id\"",{"type":24,"tag":114,"props":5429,"children":5430},{"style":1650},[5431],{"type":29,"value":1996},{"type":24,"tag":114,"props":5433,"children":5434},{"style":240},[5435],{"type":29,"value":5436},"nullable",{"type":24,"tag":114,"props":5438,"children":5439},{"style":1634},[5440],{"type":29,"value":4847},{"type":24,"tag":114,"props":5442,"children":5443},{"style":240},[5444],{"type":29,"value":5394},{"type":24,"tag":114,"props":5446,"children":5447},{"style":1650},[5448],{"type":29,"value":2005},{"type":24,"tag":114,"props":5450,"children":5451},{"class":116,"line":1846},[5452,5456],{"type":24,"tag":114,"props":5453,"children":5454},{"style":1634},[5455],{"type":29,"value":5307},{"type":24,"tag":114,"props":5457,"children":5458},{"style":1650},[5459],{"type":29,"value":5460}," Customer customer;\n",{"type":24,"tag":114,"props":5462,"children":5463},{"class":116,"line":1854},[5464],{"type":24,"tag":114,"props":5465,"children":5466},{"style":1650},[5467],{"type":29,"value":4149},{"type":24,"tag":25,"props":5469,"children":5470},{},[5471,5472,5478,5480,5486,5488,5493,5494,5499,5501,5507,5509,5515],{"type":29,"value":741},{"type":24,"tag":14,"props":5473,"children":5475},{"className":5474},[],[5476],{"type":29,"value":5477},"Customer",{"type":29,"value":5479}," maps to the ",{"type":24,"tag":14,"props":5481,"children":5483},{"className":5482},[],[5484],{"type":29,"value":5485},"customer",{"type":29,"value":5487}," table and has ",{"type":24,"tag":14,"props":5489,"children":5491},{"className":5490},[],[5492],{"type":29,"value":1930},{"type":29,"value":379},{"type":24,"tag":14,"props":5495,"children":5497},{"className":5496},[],[5498],{"type":29,"value":5250},{"type":29,"value":5500}," fields. The repository retrieves a bounded list; using ",{"type":24,"tag":14,"props":5502,"children":5504},{"className":5503},[],[5505],{"type":29,"value":5506},"List",{"type":29,"value":5508}," with ",{"type":24,"tag":14,"props":5510,"children":5512},{"className":5511},[],[5513],{"type":29,"value":5514},"Pageable",{"type":29,"value":5516}," avoids requesting a total count:",{"type":24,"tag":105,"props":5518,"children":5520},{"className":5212,"code":5519,"language":5148,"meta":7,"style":7},"public interface OrderRepository\n        extends JpaRepository\u003CPurchaseOrder, Long> {\n\n    List\u003CPurchaseOrder> findByStatusOrderByCreatedAtDescIdDesc(\n        String status, Pageable pageable);\n}\n",[5521],{"type":24,"tag":14,"props":5522,"children":5523},{"__ignoreMap":7},[5524,5541,5578,5585,5611,5637],{"type":24,"tag":114,"props":5525,"children":5526},{"class":116,"line":117},[5527,5531,5536],{"type":24,"tag":114,"props":5528,"children":5529},{"style":1634},[5530],{"type":29,"value":5271},{"type":24,"tag":114,"props":5532,"children":5533},{"style":1634},[5534],{"type":29,"value":5535}," interface",{"type":24,"tag":114,"props":5537,"children":5538},{"style":234},[5539],{"type":29,"value":5540}," OrderRepository\n",{"type":24,"tag":114,"props":5542,"children":5543},{"class":116,"line":126},[5544,5549,5554,5559,5564,5568,5573],{"type":24,"tag":114,"props":5545,"children":5546},{"style":1634},[5547],{"type":29,"value":5548},"        extends",{"type":24,"tag":114,"props":5550,"children":5551},{"style":234},[5552],{"type":29,"value":5553}," JpaRepository",{"type":24,"tag":114,"props":5555,"children":5556},{"style":1650},[5557],{"type":29,"value":5558},"\u003C",{"type":24,"tag":114,"props":5560,"children":5561},{"style":1634},[5562],{"type":29,"value":5563},"PurchaseOrder",{"type":24,"tag":114,"props":5565,"children":5566},{"style":1650},[5567],{"type":29,"value":1996},{"type":24,"tag":114,"props":5569,"children":5570},{"style":1634},[5571],{"type":29,"value":5572},"Long",{"type":24,"tag":114,"props":5574,"children":5575},{"style":1650},[5576],{"type":29,"value":5577},"> {\n",{"type":24,"tag":114,"props":5579,"children":5580},{"class":116,"line":135},[5581],{"type":24,"tag":114,"props":5582,"children":5583},{"emptyLinePlaceholder":583},[5584],{"type":29,"value":586},{"type":24,"tag":114,"props":5586,"children":5587},{"class":116,"line":144},[5588,5593,5597,5602,5607],{"type":24,"tag":114,"props":5589,"children":5590},{"style":1650},[5591],{"type":29,"value":5592},"    List\u003C",{"type":24,"tag":114,"props":5594,"children":5595},{"style":1634},[5596],{"type":29,"value":5563},{"type":24,"tag":114,"props":5598,"children":5599},{"style":1650},[5600],{"type":29,"value":5601},"> ",{"type":24,"tag":114,"props":5603,"children":5604},{"style":234},[5605],{"type":29,"value":5606},"findByStatusOrderByCreatedAtDescIdDesc",{"type":24,"tag":114,"props":5608,"children":5609},{"style":1650},[5610],{"type":29,"value":4862},{"type":24,"tag":114,"props":5612,"children":5613},{"class":116,"line":153},[5614,5619,5623,5628,5633],{"type":24,"tag":114,"props":5615,"children":5616},{"style":1650},[5617],{"type":29,"value":5618},"        String ",{"type":24,"tag":114,"props":5620,"children":5621},{"style":4096},[5622],{"type":29,"value":1938},{"type":24,"tag":114,"props":5624,"children":5625},{"style":1650},[5626],{"type":29,"value":5627},", Pageable ",{"type":24,"tag":114,"props":5629,"children":5630},{"style":4096},[5631],{"type":29,"value":5632},"pageable",{"type":24,"tag":114,"props":5634,"children":5635},{"style":1650},[5636],{"type":29,"value":1843},{"type":24,"tag":114,"props":5638,"children":5639},{"class":116,"line":162},[5640],{"type":24,"tag":114,"props":5641,"children":5642},{"style":1650},[5643],{"type":29,"value":4149},{"type":24,"tag":25,"props":5645,"children":5646},{},[5647],{"type":29,"value":5648},"The service maps those entities inside a read-only transaction:",{"type":24,"tag":105,"props":5650,"children":5652},{"className":5212,"code":5651,"language":5148,"meta":7,"style":7},"@Transactional(readOnly = true)\npublic List\u003COrderSummary> search(String status, int page, int size) {\n    var orders = repository.findByStatusOrderByCreatedAtDescIdDesc(\n        status, PageRequest.of(page, size));\n\n    return orders.stream()\n        .map(order -> new OrderSummary(\n            order.getId(),\n            order.getCustomer().getName()))\n        .toList();\n}\n",[5653],{"type":24,"tag":14,"props":5654,"children":5655},{"__ignoreMap":7},[5656,5690,5745,5775,5793,5800,5823,5859,5877,5904,5920],{"type":24,"tag":114,"props":5657,"children":5658},{"class":116,"line":117},[5659,5663,5668,5672,5677,5681,5686],{"type":24,"tag":114,"props":5660,"children":5661},{"style":1650},[5662],{"type":29,"value":4634},{"type":24,"tag":114,"props":5664,"children":5665},{"style":1634},[5666],{"type":29,"value":5667},"Transactional",{"type":24,"tag":114,"props":5669,"children":5670},{"style":1650},[5671],{"type":29,"value":4093},{"type":24,"tag":114,"props":5673,"children":5674},{"style":240},[5675],{"type":29,"value":5676},"readOnly",{"type":24,"tag":114,"props":5678,"children":5679},{"style":1634},[5680],{"type":29,"value":4847},{"type":24,"tag":114,"props":5682,"children":5683},{"style":240},[5684],{"type":29,"value":5685}," true",{"type":24,"tag":114,"props":5687,"children":5688},{"style":1650},[5689],{"type":29,"value":2005},{"type":24,"tag":114,"props":5691,"children":5692},{"class":116,"line":126},[5693,5697,5702,5706,5711,5716,5721,5726,5731,5736,5740],{"type":24,"tag":114,"props":5694,"children":5695},{"style":1634},[5696],{"type":29,"value":5271},{"type":24,"tag":114,"props":5698,"children":5699},{"style":1650},[5700],{"type":29,"value":5701}," List",{"type":24,"tag":114,"props":5703,"children":5704},{"style":1634},[5705],{"type":29,"value":5558},{"type":24,"tag":114,"props":5707,"children":5708},{"style":1650},[5709],{"type":29,"value":5710},"OrderSummary",{"type":24,"tag":114,"props":5712,"children":5713},{"style":1634},[5714],{"type":29,"value":5715},">",{"type":24,"tag":114,"props":5717,"children":5718},{"style":234},[5719],{"type":29,"value":5720}," search",{"type":24,"tag":114,"props":5722,"children":5723},{"style":1650},[5724],{"type":29,"value":5725},"(String status, ",{"type":24,"tag":114,"props":5727,"children":5728},{"style":1634},[5729],{"type":29,"value":5730},"int",{"type":24,"tag":114,"props":5732,"children":5733},{"style":1650},[5734],{"type":29,"value":5735}," page, ",{"type":24,"tag":114,"props":5737,"children":5738},{"style":1634},[5739],{"type":29,"value":5730},{"type":24,"tag":114,"props":5741,"children":5742},{"style":1650},[5743],{"type":29,"value":5744}," size) {\n",{"type":24,"tag":114,"props":5746,"children":5747},{"class":116,"line":135},[5748,5753,5758,5762,5767,5771],{"type":24,"tag":114,"props":5749,"children":5750},{"style":1634},[5751],{"type":29,"value":5752},"    var",{"type":24,"tag":114,"props":5754,"children":5755},{"style":1650},[5756],{"type":29,"value":5757}," orders ",{"type":24,"tag":114,"props":5759,"children":5760},{"style":1634},[5761],{"type":29,"value":2462},{"type":24,"tag":114,"props":5763,"children":5764},{"style":1650},[5765],{"type":29,"value":5766}," repository.",{"type":24,"tag":114,"props":5768,"children":5769},{"style":234},[5770],{"type":29,"value":5606},{"type":24,"tag":114,"props":5772,"children":5773},{"style":1650},[5774],{"type":29,"value":4862},{"type":24,"tag":114,"props":5776,"children":5777},{"class":116,"line":144},[5778,5783,5788],{"type":24,"tag":114,"props":5779,"children":5780},{"style":1650},[5781],{"type":29,"value":5782},"        status, PageRequest.",{"type":24,"tag":114,"props":5784,"children":5785},{"style":234},[5786],{"type":29,"value":5787},"of",{"type":24,"tag":114,"props":5789,"children":5790},{"style":1650},[5791],{"type":29,"value":5792},"(page, size));\n",{"type":24,"tag":114,"props":5794,"children":5795},{"class":116,"line":153},[5796],{"type":24,"tag":114,"props":5797,"children":5798},{"emptyLinePlaceholder":583},[5799],{"type":29,"value":586},{"type":24,"tag":114,"props":5801,"children":5802},{"class":116,"line":162},[5803,5808,5813,5818],{"type":24,"tag":114,"props":5804,"children":5805},{"style":1634},[5806],{"type":29,"value":5807},"    return",{"type":24,"tag":114,"props":5809,"children":5810},{"style":1650},[5811],{"type":29,"value":5812}," orders.",{"type":24,"tag":114,"props":5814,"children":5815},{"style":234},[5816],{"type":29,"value":5817},"stream",{"type":24,"tag":114,"props":5819,"children":5820},{"style":1650},[5821],{"type":29,"value":5822},"()\n",{"type":24,"tag":114,"props":5824,"children":5825},{"class":116,"line":609},[5826,5831,5835,5840,5845,5850,5855],{"type":24,"tag":114,"props":5827,"children":5828},{"style":1650},[5829],{"type":29,"value":5830},"        .",{"type":24,"tag":114,"props":5832,"children":5833},{"style":234},[5834],{"type":29,"value":4122},{"type":24,"tag":114,"props":5836,"children":5837},{"style":1650},[5838],{"type":29,"value":5839},"(order ",{"type":24,"tag":114,"props":5841,"children":5842},{"style":1634},[5843],{"type":29,"value":5844},"->",{"type":24,"tag":114,"props":5846,"children":5847},{"style":1634},[5848],{"type":29,"value":5849}," new",{"type":24,"tag":114,"props":5851,"children":5852},{"style":234},[5853],{"type":29,"value":5854}," OrderSummary",{"type":24,"tag":114,"props":5856,"children":5857},{"style":1650},[5858],{"type":29,"value":4862},{"type":24,"tag":114,"props":5860,"children":5861},{"class":116,"line":621},[5862,5867,5872],{"type":24,"tag":114,"props":5863,"children":5864},{"style":1650},[5865],{"type":29,"value":5866},"            order.",{"type":24,"tag":114,"props":5868,"children":5869},{"style":234},[5870],{"type":29,"value":5871},"getId",{"type":24,"tag":114,"props":5873,"children":5874},{"style":1650},[5875],{"type":29,"value":5876},"(),\n",{"type":24,"tag":114,"props":5878,"children":5879},{"class":116,"line":1795},[5880,5884,5889,5894,5899],{"type":24,"tag":114,"props":5881,"children":5882},{"style":1650},[5883],{"type":29,"value":5866},{"type":24,"tag":114,"props":5885,"children":5886},{"style":234},[5887],{"type":29,"value":5888},"getCustomer",{"type":24,"tag":114,"props":5890,"children":5891},{"style":1650},[5892],{"type":29,"value":5893},"().",{"type":24,"tag":114,"props":5895,"children":5896},{"style":234},[5897],{"type":29,"value":5898},"getName",{"type":24,"tag":114,"props":5900,"children":5901},{"style":1650},[5902],{"type":29,"value":5903},"()))\n",{"type":24,"tag":114,"props":5905,"children":5906},{"class":116,"line":1823},[5907,5911,5916],{"type":24,"tag":114,"props":5908,"children":5909},{"style":1650},[5910],{"type":29,"value":5830},{"type":24,"tag":114,"props":5912,"children":5913},{"style":234},[5914],{"type":29,"value":5915},"toList",{"type":24,"tag":114,"props":5917,"children":5918},{"style":1650},[5919],{"type":29,"value":4945},{"type":24,"tag":114,"props":5921,"children":5922},{"class":116,"line":1837},[5923],{"type":24,"tag":114,"props":5924,"children":5925},{"style":1650},[5926],{"type":29,"value":4149},{"type":24,"tag":25,"props":5928,"children":5929},{},[5930,5932,5937],{"type":29,"value":5931},"The endpoint should validate the page number and enforce a maximum page size. ",{"type":24,"tag":14,"props":5933,"children":5935},{"className":5934},[],[5936],{"type":29,"value":5710},{"type":29,"value":5938}," is a DTO containing an order ID and customer name.",{"type":24,"tag":25,"props":5940,"children":5941},{},[5942,5944,5950],{"type":29,"value":5943},"The easy-to-miss operation is ",{"type":24,"tag":14,"props":5945,"children":5947},{"className":5946},[],[5948],{"type":29,"value":5949},"getCustomer().getName()",{"type":29,"value":5951},". If the customer has not been loaded, accessing its name can initialize a proxy and execute SQL. The stream looks like an in-memory transformation, but it can involve database round trips.",{"type":24,"tag":25,"props":5953,"children":5954},{},[5955],{"type":29,"value":5956},"For three orders with three different, uncached customers, the SQL can look like this. Values are substituted here for readability; real application queries should use bound parameters:",{"type":24,"tag":105,"props":5958,"children":5960},{"className":1622,"code":5959,"language":1624,"meta":7,"style":7},"select id, status, created_at, customer_id\nfrom purchase_order\nwhere status = 'OPEN'\norder by created_at desc, id desc\nlimit 3 offset 0;\n\nselect id, name from customer where id = 101;\nselect id, name from customer where id = 102;\nselect id, name from customer where id = 103;\n",[5961],{"type":24,"tag":14,"props":5962,"children":5963},{"__ignoreMap":7},[5964,5986,5998,6019,6047,6074,6081,6128,6172],{"type":24,"tag":114,"props":5965,"children":5966},{"class":116,"line":117},[5967,5972,5977,5981],{"type":24,"tag":114,"props":5968,"children":5969},{"style":1634},[5970],{"type":29,"value":5971},"select",{"type":24,"tag":114,"props":5973,"children":5974},{"style":1650},[5975],{"type":29,"value":5976}," id, ",{"type":24,"tag":114,"props":5978,"children":5979},{"style":1634},[5980],{"type":29,"value":1938},{"type":24,"tag":114,"props":5982,"children":5983},{"style":1650},[5984],{"type":29,"value":5985},", created_at, customer_id\n",{"type":24,"tag":114,"props":5987,"children":5988},{"class":116,"line":126},[5989,5993],{"type":24,"tag":114,"props":5990,"children":5991},{"style":1634},[5992],{"type":29,"value":4813},{"type":24,"tag":114,"props":5994,"children":5995},{"style":1650},[5996],{"type":29,"value":5997}," purchase_order\n",{"type":24,"tag":114,"props":5999,"children":6000},{"class":116,"line":135},[6001,6006,6010,6014],{"type":24,"tag":114,"props":6002,"children":6003},{"style":1634},[6004],{"type":29,"value":6005},"where",{"type":24,"tag":114,"props":6007,"children":6008},{"style":1634},[6009],{"type":29,"value":444},{"type":24,"tag":114,"props":6011,"children":6012},{"style":1634},[6013],{"type":29,"value":4847},{"type":24,"tag":114,"props":6015,"children":6016},{"style":246},[6017],{"type":29,"value":6018}," 'OPEN'\n",{"type":24,"tag":114,"props":6020,"children":6021},{"class":116,"line":144},[6022,6027,6032,6037,6042],{"type":24,"tag":114,"props":6023,"children":6024},{"style":1634},[6025],{"type":29,"value":6026},"order by",{"type":24,"tag":114,"props":6028,"children":6029},{"style":1650},[6030],{"type":29,"value":6031}," created_at ",{"type":24,"tag":114,"props":6033,"children":6034},{"style":1634},[6035],{"type":29,"value":6036},"desc",{"type":24,"tag":114,"props":6038,"children":6039},{"style":1650},[6040],{"type":29,"value":6041},", id ",{"type":24,"tag":114,"props":6043,"children":6044},{"style":1634},[6045],{"type":29,"value":6046},"desc\n",{"type":24,"tag":114,"props":6048,"children":6049},{"class":116,"line":153},[6050,6055,6060,6065,6070],{"type":24,"tag":114,"props":6051,"children":6052},{"style":1634},[6053],{"type":29,"value":6054},"limit",{"type":24,"tag":114,"props":6056,"children":6057},{"style":240},[6058],{"type":29,"value":6059}," 3",{"type":24,"tag":114,"props":6061,"children":6062},{"style":1650},[6063],{"type":29,"value":6064}," offset ",{"type":24,"tag":114,"props":6066,"children":6067},{"style":240},[6068],{"type":29,"value":6069},"0",{"type":24,"tag":114,"props":6071,"children":6072},{"style":1650},[6073],{"type":29,"value":1912},{"type":24,"tag":114,"props":6075,"children":6076},{"class":116,"line":162},[6077],{"type":24,"tag":114,"props":6078,"children":6079},{"emptyLinePlaceholder":583},[6080],{"type":29,"value":586},{"type":24,"tag":114,"props":6082,"children":6083},{"class":116,"line":609},[6084,6088,6092,6096,6101,6106,6110,6115,6119,6124],{"type":24,"tag":114,"props":6085,"children":6086},{"style":1634},[6087],{"type":29,"value":5971},{"type":24,"tag":114,"props":6089,"children":6090},{"style":1650},[6091],{"type":29,"value":5976},{"type":24,"tag":114,"props":6093,"children":6094},{"style":1634},[6095],{"type":29,"value":5250},{"type":24,"tag":114,"props":6097,"children":6098},{"style":1634},[6099],{"type":29,"value":6100}," from",{"type":24,"tag":114,"props":6102,"children":6103},{"style":1650},[6104],{"type":29,"value":6105}," customer ",{"type":24,"tag":114,"props":6107,"children":6108},{"style":1634},[6109],{"type":29,"value":6005},{"type":24,"tag":114,"props":6111,"children":6112},{"style":1650},[6113],{"type":29,"value":6114}," id ",{"type":24,"tag":114,"props":6116,"children":6117},{"style":1634},[6118],{"type":29,"value":2462},{"type":24,"tag":114,"props":6120,"children":6121},{"style":240},[6122],{"type":29,"value":6123}," 101",{"type":24,"tag":114,"props":6125,"children":6126},{"style":1650},[6127],{"type":29,"value":1912},{"type":24,"tag":114,"props":6129,"children":6130},{"class":116,"line":621},[6131,6135,6139,6143,6147,6151,6155,6159,6163,6168],{"type":24,"tag":114,"props":6132,"children":6133},{"style":1634},[6134],{"type":29,"value":5971},{"type":24,"tag":114,"props":6136,"children":6137},{"style":1650},[6138],{"type":29,"value":5976},{"type":24,"tag":114,"props":6140,"children":6141},{"style":1634},[6142],{"type":29,"value":5250},{"type":24,"tag":114,"props":6144,"children":6145},{"style":1634},[6146],{"type":29,"value":6100},{"type":24,"tag":114,"props":6148,"children":6149},{"style":1650},[6150],{"type":29,"value":6105},{"type":24,"tag":114,"props":6152,"children":6153},{"style":1634},[6154],{"type":29,"value":6005},{"type":24,"tag":114,"props":6156,"children":6157},{"style":1650},[6158],{"type":29,"value":6114},{"type":24,"tag":114,"props":6160,"children":6161},{"style":1634},[6162],{"type":29,"value":2462},{"type":24,"tag":114,"props":6164,"children":6165},{"style":240},[6166],{"type":29,"value":6167}," 102",{"type":24,"tag":114,"props":6169,"children":6170},{"style":1650},[6171],{"type":29,"value":1912},{"type":24,"tag":114,"props":6173,"children":6174},{"class":116,"line":1795},[6175,6179,6183,6187,6191,6195,6199,6203,6207,6212],{"type":24,"tag":114,"props":6176,"children":6177},{"style":1634},[6178],{"type":29,"value":5971},{"type":24,"tag":114,"props":6180,"children":6181},{"style":1650},[6182],{"type":29,"value":5976},{"type":24,"tag":114,"props":6184,"children":6185},{"style":1634},[6186],{"type":29,"value":5250},{"type":24,"tag":114,"props":6188,"children":6189},{"style":1634},[6190],{"type":29,"value":6100},{"type":24,"tag":114,"props":6192,"children":6193},{"style":1650},[6194],{"type":29,"value":6105},{"type":24,"tag":114,"props":6196,"children":6197},{"style":1634},[6198],{"type":29,"value":6005},{"type":24,"tag":114,"props":6200,"children":6201},{"style":1650},[6202],{"type":29,"value":6114},{"type":24,"tag":114,"props":6204,"children":6205},{"style":1634},[6206],{"type":29,"value":2462},{"type":24,"tag":114,"props":6208,"children":6209},{"style":240},[6210],{"type":29,"value":6211}," 103",{"type":24,"tag":114,"props":6213,"children":6214},{"style":1650},[6215],{"type":29,"value":1912},{"type":24,"tag":25,"props":6217,"children":6218},{},[6219],{"type":29,"value":6220},"Shared customers and caches can reduce that count. For this single association, think of one extra lookup per distinct customer that still needs loading. Other associations can add more queries. The useful diagnostic is the pattern: database calls grow with the results being processed.",{"type":24,"tag":25,"props":6222,"children":6223},{},[6224],{"type":29,"value":6225},"N+1 is not specific to Hibernate or PostgreSQL. Explicit repository calls inside a loop can produce exactly the same behavior.",{"type":24,"tag":50,"props":6227,"children":6229},{"id":6228},"why-individually-fast-queries-can-create-a-slow-endpoint",[6230],{"type":29,"value":6231},"Why individually fast queries can create a slow endpoint",{"type":24,"tag":25,"props":6233,"children":6234},{},[6235],{"type":29,"value":6236},"Each extra query carries work beyond finding a row: a network round trip, database execution, transferring results, and application-side processing. A primary-key lookup can be efficient while dozens of sequential lookups are still expensive together.",{"type":24,"tag":25,"props":6238,"children":6239},{},[6240],{"type":29,"value":6241},"As a deliberately simplified example, 50 additional lookups taking 4 ms each from the application's perspective add about 200 ms. Those are illustrative numbers, not measurements from the anecdote. A database-only execution time would not capture all of that cost.",{"type":24,"tag":25,"props":6243,"children":6244},{},[6245],{"type":29,"value":6246},"Under concurrent traffic, those repeated calls also consume shared capacity. Requests can hold connections for longer and other requests can spend more time waiting for the pool. N+1 can contribute to latency spikes, but a spike alone does not prove it: locks, expensive queries, garbage collection, and downstream calls are other possibilities.",{"type":24,"tag":25,"props":6248,"children":6249},{},[6250,6252],{"type":29,"value":6251},"The question to investigate is concrete: ",{"type":24,"tag":37,"props":6253,"children":6254},{},[6255],{"type":29,"value":6256},"how many database statements does one search execute, and why?",{"type":24,"tag":50,"props":6258,"children":6260},{"id":6259},"detect-it-by-following-one-complete-request",[6261],{"type":29,"value":6262},"Detect it by following one complete request",{"type":24,"tag":6264,"props":6265,"children":6267},"h3",{"id":6266},"compare-small-and-larger-result-sets",[6268],{"type":29,"value":6269},"Compare small and larger result sets",{"type":24,"tag":25,"props":6271,"children":6272},{},[6273],{"type":29,"value":6274},"Use representative data in a development or test environment. Repeat the same search with page sizes such as 5, 20, and 50, ensuring there are enough matching rows. Include different customers; a fixture where every order shares one customer can hide the pattern.",{"type":24,"tag":25,"props":6276,"children":6277},{},[6278],{"type":29,"value":6279},"For the example above, with a fresh persistence context, no relevant cache hits, and batching disabled, these are the expected counts:",{"type":24,"tag":1349,"props":6281,"children":6282},{},[6283,6310],{"type":24,"tag":1353,"props":6284,"children":6285},{},[6286],{"type":24,"tag":1357,"props":6287,"children":6288},{},[6289,6294,6300,6305],{"type":24,"tag":1361,"props":6290,"children":6291},{},[6292],{"type":29,"value":6293},"Orders returned",{"type":24,"tag":1361,"props":6295,"children":6297},{"align":6296},"right",[6298],{"type":29,"value":6299},"Initial order query",{"type":24,"tag":1361,"props":6301,"children":6302},{"align":6296},[6303],{"type":29,"value":6304},"Customer queries",{"type":24,"tag":1361,"props":6306,"children":6307},{"align":6296},[6308],{"type":29,"value":6309},"Total",{"type":24,"tag":1377,"props":6311,"children":6312},{},[6313,6334,6355],{"type":24,"tag":1357,"props":6314,"children":6315},{},[6316,6321,6325,6329],{"type":24,"tag":1384,"props":6317,"children":6318},{},[6319],{"type":29,"value":6320},"5",{"type":24,"tag":1384,"props":6322,"children":6323},{"align":6296},[6324],{"type":29,"value":2032},{"type":24,"tag":1384,"props":6326,"children":6327},{"align":6296},[6328],{"type":29,"value":6320},{"type":24,"tag":1384,"props":6330,"children":6331},{"align":6296},[6332],{"type":29,"value":6333},"6",{"type":24,"tag":1357,"props":6335,"children":6336},{},[6337,6342,6346,6350],{"type":24,"tag":1384,"props":6338,"children":6339},{},[6340],{"type":29,"value":6341},"20",{"type":24,"tag":1384,"props":6343,"children":6344},{"align":6296},[6345],{"type":29,"value":2032},{"type":24,"tag":1384,"props":6347,"children":6348},{"align":6296},[6349],{"type":29,"value":6341},{"type":24,"tag":1384,"props":6351,"children":6352},{"align":6296},[6353],{"type":29,"value":6354},"21",{"type":24,"tag":1357,"props":6356,"children":6357},{},[6358,6363,6367,6371],{"type":24,"tag":1384,"props":6359,"children":6360},{},[6361],{"type":29,"value":6362},"50",{"type":24,"tag":1384,"props":6364,"children":6365},{"align":6296},[6366],{"type":29,"value":2032},{"type":24,"tag":1384,"props":6368,"children":6369},{"align":6296},[6370],{"type":29,"value":6362},{"type":24,"tag":1384,"props":6372,"children":6373},{"align":6296},[6374],{"type":29,"value":6375},"51",{"type":24,"tag":25,"props":6377,"children":6378},{},[6379,6381,6387],{"type":29,"value":6380},"This is an explanatory model, not benchmark output. A repository returning a ",{"type":24,"tag":14,"props":6382,"children":6384},{"className":6383},[],[6385],{"type":29,"value":6386},"Page",{"type":29,"value":6388}," may also execute a count query; account for it separately. The evidence for N+1 is the repeated per-result lookup, not simply that a request executed more than one statement.",{"type":24,"tag":6264,"props":6390,"children":6392},{"id":6391},"inspect-the-sql-generated-by-that-request",[6393],{"type":29,"value":6394},"Inspect the SQL generated by that request",{"type":24,"tag":25,"props":6396,"children":6397},{},[6398],{"type":29,"value":6399},"For a Spring Boot application using Hibernate 6, a local diagnostic profile can enable SQL logging:",{"type":24,"tag":105,"props":6401,"children":6403},{"className":3729,"code":6402,"language":3731,"meta":7,"style":7},"logging:\n  level:\n    org.hibernate.SQL: DEBUG\n\nspring:\n  jpa:\n    properties:\n      hibernate:\n        format_sql: true\n",[6404],{"type":24,"tag":14,"props":6405,"children":6406},{"__ignoreMap":7},[6407,6419,6431,6448,6455,6467,6479,6491,6503],{"type":24,"tag":114,"props":6408,"children":6409},{"class":116,"line":117},[6410,6415],{"type":24,"tag":114,"props":6411,"children":6412},{"style":3741},[6413],{"type":29,"value":6414},"logging",{"type":24,"tag":114,"props":6416,"children":6417},{"style":1650},[6418],{"type":29,"value":3766},{"type":24,"tag":114,"props":6420,"children":6421},{"class":116,"line":126},[6422,6427],{"type":24,"tag":114,"props":6423,"children":6424},{"style":3741},[6425],{"type":29,"value":6426},"  level",{"type":24,"tag":114,"props":6428,"children":6429},{"style":1650},[6430],{"type":29,"value":3766},{"type":24,"tag":114,"props":6432,"children":6433},{"class":116,"line":135},[6434,6439,6443],{"type":24,"tag":114,"props":6435,"children":6436},{"style":3741},[6437],{"type":29,"value":6438},"    org.hibernate.SQL",{"type":24,"tag":114,"props":6440,"children":6441},{"style":1650},[6442],{"type":29,"value":3748},{"type":24,"tag":114,"props":6444,"children":6445},{"style":246},[6446],{"type":29,"value":6447},"DEBUG\n",{"type":24,"tag":114,"props":6449,"children":6450},{"class":116,"line":144},[6451],{"type":24,"tag":114,"props":6452,"children":6453},{"emptyLinePlaceholder":583},[6454],{"type":29,"value":586},{"type":24,"tag":114,"props":6456,"children":6457},{"class":116,"line":153},[6458,6463],{"type":24,"tag":114,"props":6459,"children":6460},{"style":3741},[6461],{"type":29,"value":6462},"spring",{"type":24,"tag":114,"props":6464,"children":6465},{"style":1650},[6466],{"type":29,"value":3766},{"type":24,"tag":114,"props":6468,"children":6469},{"class":116,"line":162},[6470,6475],{"type":24,"tag":114,"props":6471,"children":6472},{"style":3741},[6473],{"type":29,"value":6474},"  jpa",{"type":24,"tag":114,"props":6476,"children":6477},{"style":1650},[6478],{"type":29,"value":3766},{"type":24,"tag":114,"props":6480,"children":6481},{"class":116,"line":609},[6482,6487],{"type":24,"tag":114,"props":6483,"children":6484},{"style":3741},[6485],{"type":29,"value":6486},"    properties",{"type":24,"tag":114,"props":6488,"children":6489},{"style":1650},[6490],{"type":29,"value":3766},{"type":24,"tag":114,"props":6492,"children":6493},{"class":116,"line":621},[6494,6499],{"type":24,"tag":114,"props":6495,"children":6496},{"style":3741},[6497],{"type":29,"value":6498},"      hibernate",{"type":24,"tag":114,"props":6500,"children":6501},{"style":1650},[6502],{"type":29,"value":3766},{"type":24,"tag":114,"props":6504,"children":6505},{"class":116,"line":1795},[6506,6511,6515],{"type":24,"tag":114,"props":6507,"children":6508},{"style":3741},[6509],{"type":29,"value":6510},"        format_sql",{"type":24,"tag":114,"props":6512,"children":6513},{"style":1650},[6514],{"type":29,"value":3748},{"type":24,"tag":114,"props":6516,"children":6517},{"style":240},[6518],{"type":29,"value":6519},"true\n",{"type":24,"tag":25,"props":6521,"children":6522},{},[6523,6525,6531,6533,6539,6541,6548],{"type":29,"value":6524},"Run one request and look for the initial search followed by repeated queries with the same shape. If necessary, temporarily enable ",{"type":24,"tag":14,"props":6526,"children":6528},{"className":6527},[],[6529],{"type":29,"value":6530},"org.hibernate.orm.jdbc.bind",{"type":29,"value":6532}," at ",{"type":24,"tag":14,"props":6534,"children":6536},{"className":6535},[],[6537],{"type":29,"value":6538},"TRACE",{"type":29,"value":6540}," to inspect parameter changes using synthetic data. Parameter logging can expose sensitive values, so keep it scoped to the investigation. These categories are documented in ",{"type":24,"tag":94,"props":6542,"children":6545},{"href":6543,"rel":6544},"https://docs.hibernate.org/orm/6.6/introduction/html_single/#logging-generated-sql",[98],[6546],{"type":29,"value":6547},"Hibernate's SQL logging guide",{"type":29,"value":103},{"type":24,"tag":25,"props":6550,"children":6551},{},[6552],{"type":29,"value":6553},"With concurrent traffic, use request-correlated logs or a trace with JDBC instrumentation. Inspect database spans within one request rather than counting interleaved statements from the entire application. Check the instrumentation's sampling and span limits before assuming the trace contains every query.",{"type":24,"tag":25,"props":6555,"children":6556},{},[6557],{"type":29,"value":6558},"Follow the request through DTO mapping and serialization. A mapper, computed property, or serializer that traverses entities can trigger loads after the repository method returns. An open persistence context can allow those queries to happen surprisingly late.",{"type":24,"tag":6264,"props":6560,"children":6562},{"id":6561},"use-postgresql-to-corroborate-what-you-found",[6563],{"type":29,"value":6564},"Use PostgreSQL to corroborate what you found",{"type":24,"tag":25,"props":6566,"children":6567},{},[6568,6570,6576],{"type":29,"value":6569},"If ",{"type":24,"tag":14,"props":6571,"children":6573},{"className":6572},[],[6574],{"type":29,"value":6575},"pg_stat_statements",{"type":29,"value":6577}," is already configured and available to your database role, inspect frequently executed statements:",{"type":24,"tag":105,"props":6579,"children":6581},{"className":1622,"code":6580,"language":1624,"meta":7,"style":7},"select query, calls, total_exec_time, mean_exec_time, rows\nfrom pg_stat_statements\nwhere dbid = (\n    select oid from pg_database where datname = current_database()\n)\norder by calls desc\nlimit 20;\n",[6582],{"type":24,"tag":14,"props":6583,"children":6584},{"__ignoreMap":7},[6585,6602,6614,6634,6674,6681,6697],{"type":24,"tag":114,"props":6586,"children":6587},{"class":116,"line":117},[6588,6592,6597],{"type":24,"tag":114,"props":6589,"children":6590},{"style":1634},[6591],{"type":29,"value":5971},{"type":24,"tag":114,"props":6593,"children":6594},{"style":1650},[6595],{"type":29,"value":6596}," query, calls, total_exec_time, mean_exec_time, ",{"type":24,"tag":114,"props":6598,"children":6599},{"style":1634},[6600],{"type":29,"value":6601},"rows\n",{"type":24,"tag":114,"props":6603,"children":6604},{"class":116,"line":126},[6605,6609],{"type":24,"tag":114,"props":6606,"children":6607},{"style":1634},[6608],{"type":29,"value":4813},{"type":24,"tag":114,"props":6610,"children":6611},{"style":1650},[6612],{"type":29,"value":6613}," pg_stat_statements\n",{"type":24,"tag":114,"props":6615,"children":6616},{"class":116,"line":135},[6617,6621,6626,6630],{"type":24,"tag":114,"props":6618,"children":6619},{"style":1634},[6620],{"type":29,"value":6005},{"type":24,"tag":114,"props":6622,"children":6623},{"style":1634},[6624],{"type":29,"value":6625}," dbid",{"type":24,"tag":114,"props":6627,"children":6628},{"style":1634},[6629],{"type":29,"value":4847},{"type":24,"tag":114,"props":6631,"children":6632},{"style":1650},[6633],{"type":29,"value":1653},{"type":24,"tag":114,"props":6635,"children":6636},{"class":116,"line":144},[6637,6642,6647,6651,6656,6660,6665,6669],{"type":24,"tag":114,"props":6638,"children":6639},{"style":1634},[6640],{"type":29,"value":6641},"    select",{"type":24,"tag":114,"props":6643,"children":6644},{"style":1634},[6645],{"type":29,"value":6646}," oid",{"type":24,"tag":114,"props":6648,"children":6649},{"style":1634},[6650],{"type":29,"value":6100},{"type":24,"tag":114,"props":6652,"children":6653},{"style":1650},[6654],{"type":29,"value":6655}," pg_database ",{"type":24,"tag":114,"props":6657,"children":6658},{"style":1634},[6659],{"type":29,"value":6005},{"type":24,"tag":114,"props":6661,"children":6662},{"style":1650},[6663],{"type":29,"value":6664}," datname ",{"type":24,"tag":114,"props":6666,"children":6667},{"style":1634},[6668],{"type":29,"value":2462},{"type":24,"tag":114,"props":6670,"children":6671},{"style":1650},[6672],{"type":29,"value":6673}," current_database()\n",{"type":24,"tag":114,"props":6675,"children":6676},{"class":116,"line":153},[6677],{"type":24,"tag":114,"props":6678,"children":6679},{"style":1650},[6680],{"type":29,"value":2005},{"type":24,"tag":114,"props":6682,"children":6683},{"class":116,"line":162},[6684,6688,6693],{"type":24,"tag":114,"props":6685,"children":6686},{"style":1634},[6687],{"type":29,"value":6026},{"type":24,"tag":114,"props":6689,"children":6690},{"style":1650},[6691],{"type":29,"value":6692}," calls ",{"type":24,"tag":114,"props":6694,"children":6695},{"style":1634},[6696],{"type":29,"value":6046},{"type":24,"tag":114,"props":6698,"children":6699},{"class":116,"line":609},[6700,6704,6709],{"type":24,"tag":114,"props":6701,"children":6702},{"style":1634},[6703],{"type":29,"value":6054},{"type":24,"tag":114,"props":6705,"children":6706},{"style":240},[6707],{"type":29,"value":6708}," 20",{"type":24,"tag":114,"props":6710,"children":6711},{"style":1650},[6712],{"type":29,"value":1912},{"type":24,"tag":25,"props":6714,"children":6715},{},[6716,6718,6725],{"type":29,"value":6717},"Look for repeated lookup shapes with high call counts. Compare counter deltas across a controlled observation window; cumulative totals alone do not identify a particular request. The extension aggregates statements across executions, so it corroborates application evidence rather than proving which endpoint caused the calls. Its execution-time fields measure database work, not full HTTP latency. See the ",{"type":24,"tag":94,"props":6719,"children":6722},{"href":6720,"rel":6721},"https://www.postgresql.org/docs/current/pgstatstatements.html",[98],[6723],{"type":29,"value":6724},"PostgreSQL reference",{"type":29,"value":6726}," for setup, permissions, and field definitions.",{"type":24,"tag":25,"props":6728,"children":6729},{},[6730],{"type":29,"value":6731},"For an individual lookup, inspect its plan with representative values:",{"type":24,"tag":105,"props":6733,"children":6735},{"className":1622,"code":6734,"language":1624,"meta":7,"style":7},"EXPLAIN (ANALYZE, BUFFERS)\nSELECT id, name FROM customer WHERE id = 101;\n",[6736],{"type":24,"tag":14,"props":6737,"children":6738},{"__ignoreMap":7},[6739,6747],{"type":24,"tag":114,"props":6740,"children":6741},{"class":116,"line":117},[6742],{"type":24,"tag":114,"props":6743,"children":6744},{"style":1650},[6745],{"type":29,"value":6746},"EXPLAIN (ANALYZE, BUFFERS)\n",{"type":24,"tag":114,"props":6748,"children":6749},{"class":116,"line":126},[6750,6754,6758,6762,6767,6771,6775,6779,6783,6787],{"type":24,"tag":114,"props":6751,"children":6752},{"style":1634},[6753],{"type":29,"value":2318},{"type":24,"tag":114,"props":6755,"children":6756},{"style":1650},[6757],{"type":29,"value":5976},{"type":24,"tag":114,"props":6759,"children":6760},{"style":1634},[6761],{"type":29,"value":5250},{"type":24,"tag":114,"props":6763,"children":6764},{"style":1634},[6765],{"type":29,"value":6766}," FROM",{"type":24,"tag":114,"props":6768,"children":6769},{"style":1650},[6770],{"type":29,"value":6105},{"type":24,"tag":114,"props":6772,"children":6773},{"style":1634},[6774],{"type":29,"value":2352},{"type":24,"tag":114,"props":6776,"children":6777},{"style":1650},[6778],{"type":29,"value":6114},{"type":24,"tag":114,"props":6780,"children":6781},{"style":1634},[6782],{"type":29,"value":2462},{"type":24,"tag":114,"props":6784,"children":6785},{"style":240},[6786],{"type":29,"value":6123},{"type":24,"tag":114,"props":6788,"children":6789},{"style":1650},[6790],{"type":29,"value":1912},{"type":24,"tag":25,"props":6792,"children":6793},{},[6794,6800,6802,6809],{"type":24,"tag":14,"props":6795,"children":6797},{"className":6796},[],[6798],{"type":29,"value":6799},"ANALYZE",{"type":29,"value":6801}," executes the statement, so use a suitable environment and workload. It can reveal an inefficient lookup, but even an excellent plan cannot show that the application unnecessarily executed similar queries 50 times. ",{"type":24,"tag":94,"props":6803,"children":6806},{"href":6804,"rel":6805},"https://www.postgresql.org/docs/current/using-explain.html",[98],[6807],{"type":29,"value":6808},"PostgreSQL's EXPLAIN guide",{"type":29,"value":6810}," explains how to interpret that plan.",{"type":24,"tag":50,"props":6812,"children":6814},{"id":6813},"choose-a-solution-around-the-response-you-need",[6815],{"type":29,"value":6816},"Choose a solution around the response you need",{"type":24,"tag":25,"props":6818,"children":6819},{},[6820],{"type":29,"value":6821},"Before changing annotations, write down what the endpoint must return. A search summary, an entity used for business logic, and a nested response with collections need different fetching decisions.",{"type":24,"tag":6264,"props":6823,"children":6825},{"id":6824},"return-a-dto-projection-for-a-search-summary",[6826],{"type":29,"value":6827},"Return a DTO projection for a search summary",{"type":24,"tag":25,"props":6829,"children":6830},{},[6831],{"type":29,"value":6832},"If the response only needs the order ID and customer name, query those values directly. Define the record in its own file:",{"type":24,"tag":105,"props":6834,"children":6836},{"className":5212,"code":6835,"language":5148,"meta":7,"style":7},"package com.example.orders;\n\npublic record OrderSummary(Long id, String customerName) {}\n",[6837],{"type":24,"tag":14,"props":6838,"children":6839},{"__ignoreMap":7},[6840,6853,6860],{"type":24,"tag":114,"props":6841,"children":6842},{"class":116,"line":117},[6843,6848],{"type":24,"tag":114,"props":6844,"children":6845},{"style":1634},[6846],{"type":29,"value":6847},"package",{"type":24,"tag":114,"props":6849,"children":6850},{"style":1650},[6851],{"type":29,"value":6852}," com.example.orders;\n",{"type":24,"tag":114,"props":6854,"children":6855},{"class":116,"line":126},[6856],{"type":24,"tag":114,"props":6857,"children":6858},{"emptyLinePlaceholder":583},[6859],{"type":29,"value":586},{"type":24,"tag":114,"props":6861,"children":6862},{"class":116,"line":135},[6863,6867,6872,6876],{"type":24,"tag":114,"props":6864,"children":6865},{"style":1634},[6866],{"type":29,"value":5271},{"type":24,"tag":114,"props":6868,"children":6869},{"style":1634},[6870],{"type":29,"value":6871}," record",{"type":24,"tag":114,"props":6873,"children":6874},{"style":234},[6875],{"type":29,"value":5854},{"type":24,"tag":114,"props":6877,"children":6878},{"style":1650},[6879],{"type":29,"value":6880},"(Long id, String customerName) {}\n",{"type":24,"tag":25,"props":6882,"children":6883},{},[6884],{"type":29,"value":6885},"Then replace the entity query with an explicit constructor projection:",{"type":24,"tag":105,"props":6887,"children":6889},{"className":5212,"code":6888,"language":5148,"meta":7,"style":7},"@Query(\"\"\"\n    select new com.example.orders.OrderSummary(o.id, c.name)\n    from PurchaseOrder o\n    join o.customer c\n    where o.status = :status\n    order by o.createdAt desc, o.id desc\n    \"\"\")\nList\u003COrderSummary> searchSummaries(\n    @Param(\"status\") String status, Pageable pageable);\n",[6890],{"type":24,"tag":14,"props":6891,"children":6892},{"__ignoreMap":7},[6893,6914,6922,6930,6938,6946,6954,6966,6994],{"type":24,"tag":114,"props":6894,"children":6895},{"class":116,"line":117},[6896,6900,6905,6909],{"type":24,"tag":114,"props":6897,"children":6898},{"style":1650},[6899],{"type":29,"value":4634},{"type":24,"tag":114,"props":6901,"children":6902},{"style":1634},[6903],{"type":29,"value":6904},"Query",{"type":24,"tag":114,"props":6906,"children":6907},{"style":1650},[6908],{"type":29,"value":4093},{"type":24,"tag":114,"props":6910,"children":6911},{"style":246},[6912],{"type":29,"value":6913},"\"\"\"\n",{"type":24,"tag":114,"props":6915,"children":6916},{"class":116,"line":126},[6917],{"type":24,"tag":114,"props":6918,"children":6919},{"style":246},[6920],{"type":29,"value":6921},"    select new com.example.orders.OrderSummary(o.id, c.name)\n",{"type":24,"tag":114,"props":6923,"children":6924},{"class":116,"line":135},[6925],{"type":24,"tag":114,"props":6926,"children":6927},{"style":246},[6928],{"type":29,"value":6929},"    from PurchaseOrder o\n",{"type":24,"tag":114,"props":6931,"children":6932},{"class":116,"line":144},[6933],{"type":24,"tag":114,"props":6934,"children":6935},{"style":246},[6936],{"type":29,"value":6937},"    join o.customer c\n",{"type":24,"tag":114,"props":6939,"children":6940},{"class":116,"line":153},[6941],{"type":24,"tag":114,"props":6942,"children":6943},{"style":246},[6944],{"type":29,"value":6945},"    where o.status = :status\n",{"type":24,"tag":114,"props":6947,"children":6948},{"class":116,"line":162},[6949],{"type":24,"tag":114,"props":6950,"children":6951},{"style":246},[6952],{"type":29,"value":6953},"    order by o.createdAt desc, o.id desc\n",{"type":24,"tag":114,"props":6955,"children":6956},{"class":116,"line":609},[6957,6962],{"type":24,"tag":114,"props":6958,"children":6959},{"style":246},[6960],{"type":29,"value":6961},"    \"\"\"",{"type":24,"tag":114,"props":6963,"children":6964},{"style":1650},[6965],{"type":29,"value":2005},{"type":24,"tag":114,"props":6967,"children":6968},{"class":116,"line":621},[6969,6973,6977,6981,6985,6990],{"type":24,"tag":114,"props":6970,"children":6971},{"style":1650},[6972],{"type":29,"value":5506},{"type":24,"tag":114,"props":6974,"children":6975},{"style":1634},[6976],{"type":29,"value":5558},{"type":24,"tag":114,"props":6978,"children":6979},{"style":1650},[6980],{"type":29,"value":5710},{"type":24,"tag":114,"props":6982,"children":6983},{"style":1634},[6984],{"type":29,"value":5715},{"type":24,"tag":114,"props":6986,"children":6987},{"style":234},[6988],{"type":29,"value":6989}," searchSummaries",{"type":24,"tag":114,"props":6991,"children":6992},{"style":1650},[6993],{"type":29,"value":4862},{"type":24,"tag":114,"props":6995,"children":6996},{"class":116,"line":1795},[6997,7001,7006,7010,7014],{"type":24,"tag":114,"props":6998,"children":6999},{"style":1650},[7000],{"type":29,"value":5294},{"type":24,"tag":114,"props":7002,"children":7003},{"style":1634},[7004],{"type":29,"value":7005},"Param",{"type":24,"tag":114,"props":7007,"children":7008},{"style":1650},[7009],{"type":29,"value":4093},{"type":24,"tag":114,"props":7011,"children":7012},{"style":246},[7013],{"type":29,"value":3997},{"type":24,"tag":114,"props":7015,"children":7016},{"style":1650},[7017],{"type":29,"value":7018},") String status, Pageable pageable);\n",{"type":24,"tag":25,"props":7020,"children":7021},{},[7022],{"type":29,"value":7023},"The service returns this result without traversing customer entities. In this example, one bounded query provides the summary fields. The mandatory customer relationship makes the inner join appropriate; optional relationships need deliberate handling, often a left join.",{"type":24,"tag":25,"props":7025,"children":7026},{},[7027,7029,7036],{"type":29,"value":7028},"Explicit scalar DTOs make the read contract visible. Be careful when replacing them with nested projections or projections that contain entities: the fetching behavior may change. ",{"type":24,"tag":94,"props":7030,"children":7033},{"href":7031,"rel":7032},"https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html",[98],[7034],{"type":29,"value":7035},"Spring Data's projection documentation",{"type":29,"value":7037}," covers those distinctions.",{"type":24,"tag":6264,"props":7039,"children":7041},{"id":7040},"fetch-a-required-association-with-the-entities",[7042],{"type":29,"value":7043},"Fetch a required association with the entities",{"type":24,"tag":25,"props":7045,"children":7046},{},[7047],{"type":29,"value":7048},"When the service needs managed orders and their customers, declare that requirement in the query:",{"type":24,"tag":105,"props":7050,"children":7052},{"className":5212,"code":7051,"language":5148,"meta":7,"style":7},"@Query(\"\"\"\n    select o\n    from PurchaseOrder o\n    join fetch o.customer\n    where o.status = :status\n    order by o.createdAt desc, o.id desc\n    \"\"\")\nList\u003CPurchaseOrder> searchWithCustomers(\n    @Param(\"status\") String status, Pageable pageable);\n",[7053],{"type":24,"tag":14,"props":7054,"children":7055},{"__ignoreMap":7},[7056,7075,7083,7090,7098,7105,7112,7123,7151],{"type":24,"tag":114,"props":7057,"children":7058},{"class":116,"line":117},[7059,7063,7067,7071],{"type":24,"tag":114,"props":7060,"children":7061},{"style":1650},[7062],{"type":29,"value":4634},{"type":24,"tag":114,"props":7064,"children":7065},{"style":1634},[7066],{"type":29,"value":6904},{"type":24,"tag":114,"props":7068,"children":7069},{"style":1650},[7070],{"type":29,"value":4093},{"type":24,"tag":114,"props":7072,"children":7073},{"style":246},[7074],{"type":29,"value":6913},{"type":24,"tag":114,"props":7076,"children":7077},{"class":116,"line":126},[7078],{"type":24,"tag":114,"props":7079,"children":7080},{"style":246},[7081],{"type":29,"value":7082},"    select o\n",{"type":24,"tag":114,"props":7084,"children":7085},{"class":116,"line":135},[7086],{"type":24,"tag":114,"props":7087,"children":7088},{"style":246},[7089],{"type":29,"value":6929},{"type":24,"tag":114,"props":7091,"children":7092},{"class":116,"line":144},[7093],{"type":24,"tag":114,"props":7094,"children":7095},{"style":246},[7096],{"type":29,"value":7097},"    join fetch o.customer\n",{"type":24,"tag":114,"props":7099,"children":7100},{"class":116,"line":153},[7101],{"type":24,"tag":114,"props":7102,"children":7103},{"style":246},[7104],{"type":29,"value":6945},{"type":24,"tag":114,"props":7106,"children":7107},{"class":116,"line":162},[7108],{"type":24,"tag":114,"props":7109,"children":7110},{"style":246},[7111],{"type":29,"value":6953},{"type":24,"tag":114,"props":7113,"children":7114},{"class":116,"line":609},[7115,7119],{"type":24,"tag":114,"props":7116,"children":7117},{"style":246},[7118],{"type":29,"value":6961},{"type":24,"tag":114,"props":7120,"children":7121},{"style":1650},[7122],{"type":29,"value":2005},{"type":24,"tag":114,"props":7124,"children":7125},{"class":116,"line":621},[7126,7130,7134,7138,7142,7147],{"type":24,"tag":114,"props":7127,"children":7128},{"style":1650},[7129],{"type":29,"value":5506},{"type":24,"tag":114,"props":7131,"children":7132},{"style":1634},[7133],{"type":29,"value":5558},{"type":24,"tag":114,"props":7135,"children":7136},{"style":1650},[7137],{"type":29,"value":5563},{"type":24,"tag":114,"props":7139,"children":7140},{"style":1634},[7141],{"type":29,"value":5715},{"type":24,"tag":114,"props":7143,"children":7144},{"style":234},[7145],{"type":29,"value":7146}," searchWithCustomers",{"type":24,"tag":114,"props":7148,"children":7149},{"style":1650},[7150],{"type":29,"value":4862},{"type":24,"tag":114,"props":7152,"children":7153},{"class":116,"line":1795},[7154,7158,7162,7166,7170],{"type":24,"tag":114,"props":7155,"children":7156},{"style":1650},[7157],{"type":29,"value":5294},{"type":24,"tag":114,"props":7159,"children":7160},{"style":1634},[7161],{"type":29,"value":7005},{"type":24,"tag":114,"props":7163,"children":7164},{"style":1650},[7165],{"type":29,"value":4093},{"type":24,"tag":114,"props":7167,"children":7168},{"style":246},[7169],{"type":29,"value":3997},{"type":24,"tag":114,"props":7171,"children":7172},{"style":1650},[7173],{"type":29,"value":7018},{"type":24,"tag":25,"props":7175,"children":7176},{},[7177,7179,7184,7186,7193],{"type":29,"value":7178},"This fetches a ",{"type":24,"tag":37,"props":7180,"children":7181},{},[7182],{"type":29,"value":7183},"to-one",{"type":29,"value":7185}," relationship: each order has one customer, so that join does not multiply order rows. Mapping the customer name no longer requires a separate customer lookup. Verify other accessed associations as well. The ",{"type":24,"tag":94,"props":7187,"children":7190},{"href":7188,"rel":7189},"https://docs.hibernate.org/orm/6.6/querylanguage/html_single/#join-fetch",[98],[7191],{"type":29,"value":7192},"Hibernate query guide",{"type":29,"value":7194}," describes fetch joins and their limitations.",{"type":24,"tag":25,"props":7196,"children":7197},{},[7198],{"type":29,"value":7199},"An alternative for the original repository method is a fetch graph:",{"type":24,"tag":105,"props":7201,"children":7203},{"className":5212,"code":7202,"language":5148,"meta":7,"style":7},"@EntityGraph(attributePaths = \"customer\")\nList\u003CPurchaseOrder> findByStatusOrderByCreatedAtDescIdDesc(\n    String status, Pageable pageable);\n",[7204],{"type":24,"tag":14,"props":7205,"children":7206},{"__ignoreMap":7},[7207,7241,7269],{"type":24,"tag":114,"props":7208,"children":7209},{"class":116,"line":117},[7210,7214,7219,7223,7228,7232,7237],{"type":24,"tag":114,"props":7211,"children":7212},{"style":1650},[7213],{"type":29,"value":4634},{"type":24,"tag":114,"props":7215,"children":7216},{"style":1634},[7217],{"type":29,"value":7218},"EntityGraph",{"type":24,"tag":114,"props":7220,"children":7221},{"style":1650},[7222],{"type":29,"value":4093},{"type":24,"tag":114,"props":7224,"children":7225},{"style":240},[7226],{"type":29,"value":7227},"attributePaths",{"type":24,"tag":114,"props":7229,"children":7230},{"style":1634},[7231],{"type":29,"value":4847},{"type":24,"tag":114,"props":7233,"children":7234},{"style":246},[7235],{"type":29,"value":7236}," \"customer\"",{"type":24,"tag":114,"props":7238,"children":7239},{"style":1650},[7240],{"type":29,"value":2005},{"type":24,"tag":114,"props":7242,"children":7243},{"class":116,"line":126},[7244,7248,7252,7256,7260,7265],{"type":24,"tag":114,"props":7245,"children":7246},{"style":1650},[7247],{"type":29,"value":5506},{"type":24,"tag":114,"props":7249,"children":7250},{"style":1634},[7251],{"type":29,"value":5558},{"type":24,"tag":114,"props":7253,"children":7254},{"style":1650},[7255],{"type":29,"value":5563},{"type":24,"tag":114,"props":7257,"children":7258},{"style":1634},[7259],{"type":29,"value":5715},{"type":24,"tag":114,"props":7261,"children":7262},{"style":234},[7263],{"type":29,"value":7264}," findByStatusOrderByCreatedAtDescIdDesc",{"type":24,"tag":114,"props":7266,"children":7267},{"style":1650},[7268],{"type":29,"value":4862},{"type":24,"tag":114,"props":7270,"children":7271},{"class":116,"line":135},[7272],{"type":24,"tag":114,"props":7273,"children":7274},{"style":1650},[7275],{"type":29,"value":7276},"    String status, Pageable pageable);\n",{"type":24,"tag":25,"props":7278,"children":7279},{},[7280,7282,7289],{"type":29,"value":7281},"An entity graph declares which attributes to fetch. Inspect the generated SQL rather than treating it as a universal guarantee of one statement; see ",{"type":24,"tag":94,"props":7283,"children":7286},{"href":7284,"rel":7285},"https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.entity-graph",[98],[7287],{"type":29,"value":7288},"Spring Data's entity graph support",{"type":29,"value":103},{"type":24,"tag":25,"props":7291,"children":7292},{},[7293,7295,7301],{"type":29,"value":7294},"Changing every association to ",{"type":24,"tag":14,"props":7296,"children":7298},{"className":7297},[],[7299],{"type":29,"value":7300},"EAGER",{"type":29,"value":7302}," is not a reliable fix. Eager loading can still use secondary selects and it changes the loading requirements for other use cases too. Make the fetching choice where the endpoint's needs are known.",{"type":24,"tag":6264,"props":7304,"children":7306},{"id":7305},"batch-association-loads-when-a-join-is-unsuitable",[7307],{"type":29,"value":7308},"Batch association loads when a join is unsuitable",{"type":24,"tag":25,"props":7310,"children":7311},{},[7312],{"type":29,"value":7313},"Hibernate can load several pending associations together. One way to enable this is:",{"type":24,"tag":105,"props":7315,"children":7317},{"className":3729,"code":7316,"language":3731,"meta":7,"style":7},"spring:\n  jpa:\n    properties:\n      hibernate:\n        default_batch_fetch_size: 32\n",[7318],{"type":24,"tag":14,"props":7319,"children":7320},{"__ignoreMap":7},[7321,7332,7343,7354,7365],{"type":24,"tag":114,"props":7322,"children":7323},{"class":116,"line":117},[7324,7328],{"type":24,"tag":114,"props":7325,"children":7326},{"style":3741},[7327],{"type":29,"value":6462},{"type":24,"tag":114,"props":7329,"children":7330},{"style":1650},[7331],{"type":29,"value":3766},{"type":24,"tag":114,"props":7333,"children":7334},{"class":116,"line":126},[7335,7339],{"type":24,"tag":114,"props":7336,"children":7337},{"style":3741},[7338],{"type":29,"value":6474},{"type":24,"tag":114,"props":7340,"children":7341},{"style":1650},[7342],{"type":29,"value":3766},{"type":24,"tag":114,"props":7344,"children":7345},{"class":116,"line":135},[7346,7350],{"type":24,"tag":114,"props":7347,"children":7348},{"style":3741},[7349],{"type":29,"value":6486},{"type":24,"tag":114,"props":7351,"children":7352},{"style":1650},[7353],{"type":29,"value":3766},{"type":24,"tag":114,"props":7355,"children":7356},{"class":116,"line":144},[7357,7361],{"type":24,"tag":114,"props":7358,"children":7359},{"style":3741},[7360],{"type":29,"value":6498},{"type":24,"tag":114,"props":7362,"children":7363},{"style":1650},[7364],{"type":29,"value":3766},{"type":24,"tag":114,"props":7366,"children":7367},{"class":116,"line":153},[7368,7373,7377],{"type":24,"tag":114,"props":7369,"children":7370},{"style":3741},[7371],{"type":29,"value":7372},"        default_batch_fetch_size",{"type":24,"tag":114,"props":7374,"children":7375},{"style":1650},[7376],{"type":29,"value":3748},{"type":24,"tag":114,"props":7378,"children":7379},{"style":240},[7380],{"type":29,"value":7381},"32\n",{"type":24,"tag":25,"props":7383,"children":7384},{},[7385],{"type":29,"value":7386},"For 50 distinct uncached customers already referenced in one persistence context, a batch size of 32 might reduce the customer lookups to two batches. Including the order query, that would be three statements. Treat this as an expected shape to verify, not a guaranteed count: access order, mappings, caches, and eligible proxies affect batching.",{"type":24,"tag":25,"props":7388,"children":7389},{},[7390,7392,7398,7400,7407],{"type":29,"value":7391},"Batch fetching still materializes entities and needs tuning. It can be useful when changing the fetch query is awkward or when fetching collections separately avoids excessive row duplication. It is different from ",{"type":24,"tag":14,"props":7393,"children":7395},{"className":7394},[],[7396],{"type":29,"value":7397},"hibernate.jdbc.batch_size",{"type":29,"value":7399},", which concerns batching writes. Hibernate documents the distinction in its ",{"type":24,"tag":94,"props":7401,"children":7404},{"href":7402,"rel":7403},"https://docs.hibernate.org/orm/6.6/userguide/html_single/#fetching-batch",[98],[7405],{"type":29,"value":7406},"fetching guide",{"type":29,"value":103},{"type":24,"tag":25,"props":7409,"children":7410},{},[7411],{"type":29,"value":7412},"You can also make batching explicit: collect the required IDs, issue a bounded bulk query, and assemble the response from a map. That approach works without relying on ORM proxy loading.",{"type":24,"tag":50,"props":7414,"children":7416},{"id":7415},"be-especially-careful-with-collections-and-pagination",[7417],{"type":29,"value":7418},"Be especially careful with collections and pagination",{"type":24,"tag":25,"props":7420,"children":7421},{},[7422],{"type":29,"value":7423},"Suppose the search now needs order line items. Joining an order to ten items produces ten SQL rows for that order. Applying a row limit to that result is not the same as selecting a page of orders.",{"type":24,"tag":25,"props":7425,"children":7426},{},[7427,7429,7435],{"type":29,"value":7428},"Hibernate may handle a collection fetch join with pagination by loading all matching rows and paginating in memory. Multiple collection joins can also multiply the result set. A lower statement count is not automatically a faster query. The ",{"type":24,"tag":94,"props":7430,"children":7432},{"href":7188,"rel":7431},[98],[7433],{"type":29,"value":7434},"fetch join documentation",{"type":29,"value":7436}," calls out these restrictions.",{"type":24,"tag":25,"props":7438,"children":7439},{},[7440],{"type":29,"value":7441},"For Hibernate, enable this guard to fail when collection fetching would require in-memory pagination:",{"type":24,"tag":105,"props":7443,"children":7445},{"className":3729,"code":7444,"language":3731,"meta":7,"style":7},"spring:\n  jpa:\n    properties:\n      hibernate:\n        query:\n          fail_on_pagination_over_collection_fetch: true\n",[7446],{"type":24,"tag":14,"props":7447,"children":7448},{"__ignoreMap":7},[7449,7460,7471,7482,7493,7505],{"type":24,"tag":114,"props":7450,"children":7451},{"class":116,"line":117},[7452,7456],{"type":24,"tag":114,"props":7453,"children":7454},{"style":3741},[7455],{"type":29,"value":6462},{"type":24,"tag":114,"props":7457,"children":7458},{"style":1650},[7459],{"type":29,"value":3766},{"type":24,"tag":114,"props":7461,"children":7462},{"class":116,"line":126},[7463,7467],{"type":24,"tag":114,"props":7464,"children":7465},{"style":3741},[7466],{"type":29,"value":6474},{"type":24,"tag":114,"props":7468,"children":7469},{"style":1650},[7470],{"type":29,"value":3766},{"type":24,"tag":114,"props":7472,"children":7473},{"class":116,"line":135},[7474,7478],{"type":24,"tag":114,"props":7475,"children":7476},{"style":3741},[7477],{"type":29,"value":6486},{"type":24,"tag":114,"props":7479,"children":7480},{"style":1650},[7481],{"type":29,"value":3766},{"type":24,"tag":114,"props":7483,"children":7484},{"class":116,"line":144},[7485,7489],{"type":24,"tag":114,"props":7486,"children":7487},{"style":3741},[7488],{"type":29,"value":6498},{"type":24,"tag":114,"props":7490,"children":7491},{"style":1650},[7492],{"type":29,"value":3766},{"type":24,"tag":114,"props":7494,"children":7495},{"class":116,"line":153},[7496,7501],{"type":24,"tag":114,"props":7497,"children":7498},{"style":3741},[7499],{"type":29,"value":7500},"        query",{"type":24,"tag":114,"props":7502,"children":7503},{"style":1650},[7504],{"type":29,"value":3766},{"type":24,"tag":114,"props":7506,"children":7507},{"class":116,"line":162},[7508,7513,7517],{"type":24,"tag":114,"props":7509,"children":7510},{"style":3741},[7511],{"type":29,"value":7512},"          fail_on_pagination_over_collection_fetch",{"type":24,"tag":114,"props":7514,"children":7515},{"style":1650},[7516],{"type":29,"value":3748},{"type":24,"tag":114,"props":7518,"children":7519},{"style":240},[7520],{"type":29,"value":6519},{"type":24,"tag":25,"props":7522,"children":7523},{},[7524,7526,7533],{"type":29,"value":7525},"The setting's behavior is described in ",{"type":24,"tag":94,"props":7527,"children":7530},{"href":7528,"rel":7529},"https://docs.hibernate.org/orm/6.6/javadocs/org/hibernate/cfg/QuerySettings.html#FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH",[98],[7531],{"type":29,"value":7532},"Hibernate's query settings",{"type":29,"value":103},{"type":24,"tag":25,"props":7535,"children":7536},{},[7537],{"type":29,"value":7538},"A practical alternative is to page the parent IDs first, then fetch the data for that bounded set:",{"type":24,"tag":105,"props":7540,"children":7542},{"className":1622,"code":7541,"language":1624,"meta":7,"style":7},"-- First: choose the page of orders.\nselect id\nfrom purchase_order\nwhere status = 'OPEN'\norder by created_at desc, id desc\nlimit 20 offset 0;\n\n-- Second: fetch details only for the IDs returned above.\n-- The IDs below are illustrative bound-parameter values.\nselect o.id, c.name, i.id as item_id, i.quantity\nfrom purchase_order o\njoin customer c on c.id = o.customer_id\nleft join order_item i on i.order_id = o.id\nwhere o.id in (101, 102, 103)\norder by o.created_at desc, o.id desc, i.id;\n",[7543],{"type":24,"tag":14,"props":7544,"children":7545},{"__ignoreMap":7},[7546,7554,7566,7577,7596,7619,7642,7649,7657,7665,7742,7754,7802,7849,7904],{"type":24,"tag":114,"props":7547,"children":7548},{"class":116,"line":117},[7549],{"type":24,"tag":114,"props":7550,"children":7551},{"style":460},[7552],{"type":29,"value":7553},"-- First: choose the page of orders.\n",{"type":24,"tag":114,"props":7555,"children":7556},{"class":116,"line":126},[7557,7561],{"type":24,"tag":114,"props":7558,"children":7559},{"style":1634},[7560],{"type":29,"value":5971},{"type":24,"tag":114,"props":7562,"children":7563},{"style":1650},[7564],{"type":29,"value":7565}," id\n",{"type":24,"tag":114,"props":7567,"children":7568},{"class":116,"line":135},[7569,7573],{"type":24,"tag":114,"props":7570,"children":7571},{"style":1634},[7572],{"type":29,"value":4813},{"type":24,"tag":114,"props":7574,"children":7575},{"style":1650},[7576],{"type":29,"value":5997},{"type":24,"tag":114,"props":7578,"children":7579},{"class":116,"line":144},[7580,7584,7588,7592],{"type":24,"tag":114,"props":7581,"children":7582},{"style":1634},[7583],{"type":29,"value":6005},{"type":24,"tag":114,"props":7585,"children":7586},{"style":1634},[7587],{"type":29,"value":444},{"type":24,"tag":114,"props":7589,"children":7590},{"style":1634},[7591],{"type":29,"value":4847},{"type":24,"tag":114,"props":7593,"children":7594},{"style":246},[7595],{"type":29,"value":6018},{"type":24,"tag":114,"props":7597,"children":7598},{"class":116,"line":153},[7599,7603,7607,7611,7615],{"type":24,"tag":114,"props":7600,"children":7601},{"style":1634},[7602],{"type":29,"value":6026},{"type":24,"tag":114,"props":7604,"children":7605},{"style":1650},[7606],{"type":29,"value":6031},{"type":24,"tag":114,"props":7608,"children":7609},{"style":1634},[7610],{"type":29,"value":6036},{"type":24,"tag":114,"props":7612,"children":7613},{"style":1650},[7614],{"type":29,"value":6041},{"type":24,"tag":114,"props":7616,"children":7617},{"style":1634},[7618],{"type":29,"value":6046},{"type":24,"tag":114,"props":7620,"children":7621},{"class":116,"line":162},[7622,7626,7630,7634,7638],{"type":24,"tag":114,"props":7623,"children":7624},{"style":1634},[7625],{"type":29,"value":6054},{"type":24,"tag":114,"props":7627,"children":7628},{"style":240},[7629],{"type":29,"value":6708},{"type":24,"tag":114,"props":7631,"children":7632},{"style":1650},[7633],{"type":29,"value":6064},{"type":24,"tag":114,"props":7635,"children":7636},{"style":240},[7637],{"type":29,"value":6069},{"type":24,"tag":114,"props":7639,"children":7640},{"style":1650},[7641],{"type":29,"value":1912},{"type":24,"tag":114,"props":7643,"children":7644},{"class":116,"line":609},[7645],{"type":24,"tag":114,"props":7646,"children":7647},{"emptyLinePlaceholder":583},[7648],{"type":29,"value":586},{"type":24,"tag":114,"props":7650,"children":7651},{"class":116,"line":621},[7652],{"type":24,"tag":114,"props":7653,"children":7654},{"style":460},[7655],{"type":29,"value":7656},"-- Second: fetch details only for the IDs returned above.\n",{"type":24,"tag":114,"props":7658,"children":7659},{"class":116,"line":1795},[7660],{"type":24,"tag":114,"props":7661,"children":7662},{"style":460},[7663],{"type":29,"value":7664},"-- The IDs below are illustrative bound-parameter values.\n",{"type":24,"tag":114,"props":7666,"children":7667},{"class":116,"line":1823},[7668,7672,7677,7681,7685,7689,7694,7698,7702,7706,7711,7715,7719,7724,7729,7733,7737],{"type":24,"tag":114,"props":7669,"children":7670},{"style":1634},[7671],{"type":29,"value":5971},{"type":24,"tag":114,"props":7673,"children":7674},{"style":240},[7675],{"type":29,"value":7676}," o",{"type":24,"tag":114,"props":7678,"children":7679},{"style":1650},[7680],{"type":29,"value":103},{"type":24,"tag":114,"props":7682,"children":7683},{"style":240},[7684],{"type":29,"value":1930},{"type":24,"tag":114,"props":7686,"children":7687},{"style":1650},[7688],{"type":29,"value":1996},{"type":24,"tag":114,"props":7690,"children":7691},{"style":240},[7692],{"type":29,"value":7693},"c",{"type":24,"tag":114,"props":7695,"children":7696},{"style":1650},[7697],{"type":29,"value":103},{"type":24,"tag":114,"props":7699,"children":7700},{"style":240},[7701],{"type":29,"value":5250},{"type":24,"tag":114,"props":7703,"children":7704},{"style":1650},[7705],{"type":29,"value":1996},{"type":24,"tag":114,"props":7707,"children":7708},{"style":240},[7709],{"type":29,"value":7710},"i",{"type":24,"tag":114,"props":7712,"children":7713},{"style":1650},[7714],{"type":29,"value":103},{"type":24,"tag":114,"props":7716,"children":7717},{"style":240},[7718],{"type":29,"value":1930},{"type":24,"tag":114,"props":7720,"children":7721},{"style":1634},[7722],{"type":29,"value":7723}," as",{"type":24,"tag":114,"props":7725,"children":7726},{"style":1650},[7727],{"type":29,"value":7728}," item_id, ",{"type":24,"tag":114,"props":7730,"children":7731},{"style":240},[7732],{"type":29,"value":7710},{"type":24,"tag":114,"props":7734,"children":7735},{"style":1650},[7736],{"type":29,"value":103},{"type":24,"tag":114,"props":7738,"children":7739},{"style":240},[7740],{"type":29,"value":7741},"quantity\n",{"type":24,"tag":114,"props":7743,"children":7744},{"class":116,"line":1837},[7745,7749],{"type":24,"tag":114,"props":7746,"children":7747},{"style":1634},[7748],{"type":29,"value":4813},{"type":24,"tag":114,"props":7750,"children":7751},{"style":1650},[7752],{"type":29,"value":7753}," purchase_order o\n",{"type":24,"tag":114,"props":7755,"children":7756},{"class":116,"line":1846},[7757,7762,7767,7772,7777,7781,7785,7789,7793,7797],{"type":24,"tag":114,"props":7758,"children":7759},{"style":1634},[7760],{"type":29,"value":7761},"join",{"type":24,"tag":114,"props":7763,"children":7764},{"style":1650},[7765],{"type":29,"value":7766}," customer c ",{"type":24,"tag":114,"props":7768,"children":7769},{"style":1634},[7770],{"type":29,"value":7771},"on",{"type":24,"tag":114,"props":7773,"children":7774},{"style":240},[7775],{"type":29,"value":7776}," c",{"type":24,"tag":114,"props":7778,"children":7779},{"style":1650},[7780],{"type":29,"value":103},{"type":24,"tag":114,"props":7782,"children":7783},{"style":240},[7784],{"type":29,"value":1930},{"type":24,"tag":114,"props":7786,"children":7787},{"style":1634},[7788],{"type":29,"value":4847},{"type":24,"tag":114,"props":7790,"children":7791},{"style":240},[7792],{"type":29,"value":7676},{"type":24,"tag":114,"props":7794,"children":7795},{"style":1650},[7796],{"type":29,"value":103},{"type":24,"tag":114,"props":7798,"children":7799},{"style":240},[7800],{"type":29,"value":7801},"customer_id\n",{"type":24,"tag":114,"props":7803,"children":7804},{"class":116,"line":1854},[7805,7810,7815,7819,7824,7828,7832,7836,7840,7844],{"type":24,"tag":114,"props":7806,"children":7807},{"style":1634},[7808],{"type":29,"value":7809},"left join",{"type":24,"tag":114,"props":7811,"children":7812},{"style":1650},[7813],{"type":29,"value":7814}," order_item i ",{"type":24,"tag":114,"props":7816,"children":7817},{"style":1634},[7818],{"type":29,"value":7771},{"type":24,"tag":114,"props":7820,"children":7821},{"style":240},[7822],{"type":29,"value":7823}," i",{"type":24,"tag":114,"props":7825,"children":7826},{"style":1650},[7827],{"type":29,"value":103},{"type":24,"tag":114,"props":7829,"children":7830},{"style":240},[7831],{"type":29,"value":2825},{"type":24,"tag":114,"props":7833,"children":7834},{"style":1634},[7835],{"type":29,"value":4847},{"type":24,"tag":114,"props":7837,"children":7838},{"style":240},[7839],{"type":29,"value":7676},{"type":24,"tag":114,"props":7841,"children":7842},{"style":1650},[7843],{"type":29,"value":103},{"type":24,"tag":114,"props":7845,"children":7846},{"style":240},[7847],{"type":29,"value":7848},"id\n",{"type":24,"tag":114,"props":7850,"children":7851},{"class":116,"line":1872},[7852,7856,7860,7864,7868,7873,7877,7882,7886,7891,7895,7900],{"type":24,"tag":114,"props":7853,"children":7854},{"style":1634},[7855],{"type":29,"value":6005},{"type":24,"tag":114,"props":7857,"children":7858},{"style":240},[7859],{"type":29,"value":7676},{"type":24,"tag":114,"props":7861,"children":7862},{"style":1650},[7863],{"type":29,"value":103},{"type":24,"tag":114,"props":7865,"children":7866},{"style":240},[7867],{"type":29,"value":1930},{"type":24,"tag":114,"props":7869,"children":7870},{"style":1634},[7871],{"type":29,"value":7872}," in",{"type":24,"tag":114,"props":7874,"children":7875},{"style":1650},[7876],{"type":29,"value":2907},{"type":24,"tag":114,"props":7878,"children":7879},{"style":240},[7880],{"type":29,"value":7881},"101",{"type":24,"tag":114,"props":7883,"children":7884},{"style":1650},[7885],{"type":29,"value":1996},{"type":24,"tag":114,"props":7887,"children":7888},{"style":240},[7889],{"type":29,"value":7890},"102",{"type":24,"tag":114,"props":7892,"children":7893},{"style":1650},[7894],{"type":29,"value":1996},{"type":24,"tag":114,"props":7896,"children":7897},{"style":240},[7898],{"type":29,"value":7899},"103",{"type":24,"tag":114,"props":7901,"children":7902},{"style":1650},[7903],{"type":29,"value":2005},{"type":24,"tag":114,"props":7905,"children":7906},{"class":116,"line":1886},[7907,7911,7915,7919,7923,7928,7932,7937,7941,7945,7949,7953,7957,7961,7965],{"type":24,"tag":114,"props":7908,"children":7909},{"style":1634},[7910],{"type":29,"value":6026},{"type":24,"tag":114,"props":7912,"children":7913},{"style":240},[7914],{"type":29,"value":7676},{"type":24,"tag":114,"props":7916,"children":7917},{"style":1650},[7918],{"type":29,"value":103},{"type":24,"tag":114,"props":7920,"children":7921},{"style":240},[7922],{"type":29,"value":3115},{"type":24,"tag":114,"props":7924,"children":7925},{"style":1634},[7926],{"type":29,"value":7927}," desc",{"type":24,"tag":114,"props":7929,"children":7930},{"style":1650},[7931],{"type":29,"value":1996},{"type":24,"tag":114,"props":7933,"children":7934},{"style":240},[7935],{"type":29,"value":7936},"o",{"type":24,"tag":114,"props":7938,"children":7939},{"style":1650},[7940],{"type":29,"value":103},{"type":24,"tag":114,"props":7942,"children":7943},{"style":240},[7944],{"type":29,"value":1930},{"type":24,"tag":114,"props":7946,"children":7947},{"style":1634},[7948],{"type":29,"value":7927},{"type":24,"tag":114,"props":7950,"children":7951},{"style":1650},[7952],{"type":29,"value":1996},{"type":24,"tag":114,"props":7954,"children":7955},{"style":240},[7956],{"type":29,"value":7710},{"type":24,"tag":114,"props":7958,"children":7959},{"style":1650},[7960],{"type":29,"value":103},{"type":24,"tag":114,"props":7962,"children":7963},{"style":240},[7964],{"type":29,"value":1930},{"type":24,"tag":114,"props":7966,"children":7967},{"style":1650},[7968],{"type":29,"value":1912},{"type":24,"tag":25,"props":7970,"children":7971},{},[7972],{"type":29,"value":7973},"Group the detail rows into response DTOs. Skip the second query for an empty page, preserve the first query's order, and carry tenant and authorization restrictions through both stages. If the client needs a total, budget for a separate count query.",{"type":24,"tag":25,"props":7975,"children":7976},{},[7977,7979,7986],{"type":29,"value":7978},"Consider consistency too: two statements at PostgreSQL's default Read Committed isolation can observe different committed data, even in one transaction. Decide whether that is acceptable for the search or whether a consistent snapshot is required. PostgreSQL documents these ",{"type":24,"tag":94,"props":7980,"children":7983},{"href":7981,"rel":7982},"https://www.postgresql.org/docs/current/transaction-iso.html",[98],[7984],{"type":29,"value":7985},"transaction isolation semantics",{"type":29,"value":103},{"type":24,"tag":25,"props":7988,"children":7989},{},[7990],{"type":29,"value":7991},"Paging parents does not bound the number of children within each parent. If a search needs only an item count or total, use an aggregate rather than loading every item. If it needs an enormous nested collection, that collection may need its own pagination.",{"type":24,"tag":50,"props":7993,"children":7995},{"id":7994},"verify-that-the-fix-survives-realistic-conditions",[7996],{"type":29,"value":7997},"Verify that the fix survives realistic conditions",{"type":24,"tag":25,"props":7999,"children":8000},{},[8001],{"type":29,"value":8002},"Repeat the original searches with the same dataset, page sizes, and concurrency. Check response correctness and ordering as carefully as latency. Record queries per request, rows fetched, connection-pool waits, and p50/p95/p99 response times alongside the average. A mean alone can hide the requests that hurt users most.",{"type":24,"tag":25,"props":8004,"children":8005},{},[8006],{"type":29,"value":8007},"Use both cold and warm application-cache conditions. A cache may hide repeated loads without removing the underlying pattern. Likewise, adding an index can improve each lookup without eliminating unnecessary round trips. Increasing the pool or parallelizing the lookups can move pressure onto PostgreSQL while leaving the cause intact.",{"type":24,"tag":25,"props":8009,"children":8010},{},[8011],{"type":29,"value":8012},"Add a focused integration check around the complete response mapping. For the scalar projection above, fetching 5 or 50 summaries should remain one select, assuming no unrelated work is included. For a batched design, assert the intended bounded growth instead of an arbitrary universal limit.",{"type":24,"tag":25,"props":8014,"children":8015},{},[8016],{"type":29,"value":8017},"Prepare and flush fixtures before measuring, start with a fresh persistence context, and control the second-level cache. Include distinct related entities. Count statements for the operation under test; global ORM statistics can mix in other requests or parallel tests. The regression to catch is a new mapper field quietly turning a bounded query into one lookup per result.",{"type":24,"tag":50,"props":8019,"children":8021},{"id":8020},"make-performance-part-of-everyday-ownership",[8022],{"type":29,"value":8023},"Make performance part of everyday ownership",{"type":24,"tag":25,"props":8025,"children":8026},{},[8027],{"type":29,"value":8028},"A useful next step is small: choose a search endpoint you maintain, call it with a representative page, and read the SQL it produces. Can you explain every query? Does the amount of database work match what the response needs?",{"type":24,"tag":25,"props":8030,"children":8031},{},[8032],{"type":29,"value":8033},"Knowing N+1 by name is a starting point. Recognizing it in familiar code, proving the cause, and checking the fix are skills we can practise and share. Our response times represent our work as a team; paying attention to them is one practical way to take care of that work and the people using it.",{"type":24,"tag":50,"props":8035,"children":8036},{"id":1181},[8037],{"type":29,"value":1184},{"type":24,"tag":1186,"props":8039,"children":8040},{},[8041,8052,8062,8072],{"type":24,"tag":1149,"props":8042,"children":8043},{},[8044,8051],{"type":24,"tag":94,"props":8045,"children":8048},{"href":8046,"rel":8047},"https://docs.hibernate.org/orm/6.6/userguide/html_single/#fetching",[98],[8049],{"type":29,"value":8050},"Hibernate: fetching strategies",{"type":29,"value":103},{"type":24,"tag":1149,"props":8053,"children":8054},{},[8055,8061],{"type":24,"tag":94,"props":8056,"children":8058},{"href":7031,"rel":8057},[98],[8059],{"type":29,"value":8060},"Spring Data JPA: projections",{"type":29,"value":103},{"type":24,"tag":1149,"props":8063,"children":8064},{},[8065,8071],{"type":24,"tag":94,"props":8066,"children":8068},{"href":6720,"rel":8067},[98],[8069],{"type":29,"value":8070},"PostgreSQL: pg_stat_statements",{"type":29,"value":103},{"type":24,"tag":1149,"props":8073,"children":8074},{},[8075,8081],{"type":24,"tag":94,"props":8076,"children":8078},{"href":6804,"rel":8077},[98],[8079],{"type":29,"value":8080},"PostgreSQL: using EXPLAIN",{"type":29,"value":103},{"type":24,"tag":1222,"props":8083,"children":8084},{},[8085],{"type":29,"value":1226},{"title":7,"searchDepth":126,"depth":126,"links":8087},[8088,8089,8090,8095,8100,8101,8102,8103],{"id":5185,"depth":126,"text":5188},{"id":6228,"depth":126,"text":6231},{"id":6259,"depth":126,"text":6262,"children":8091},[8092,8093,8094],{"id":6266,"depth":135,"text":6269},{"id":6391,"depth":135,"text":6394},{"id":6561,"depth":135,"text":6564},{"id":6813,"depth":126,"text":6816,"children":8096},[8097,8098,8099],{"id":6824,"depth":135,"text":6827},{"id":7040,"depth":135,"text":7043},{"id":7305,"depth":135,"text":7308},{"id":7415,"depth":126,"text":7418},{"id":7994,"depth":126,"text":7997},{"id":8020,"depth":126,"text":8023},{"id":1181,"depth":126,"text":1184},"content:posts:n-plus-one-queries.md","posts/n-plus-one-queries.md","posts/n-plus-one-queries",[8108,8109,8110,8111,8112,8116,8119,8123,8127,8131,8135,8139,8143,8147,8151,8155,8159,8163,8167,8171,8175,8179,8183,8187,8190,8194,8198],{"_path":4,"title":8,"date":11},{"_path":1245,"title":1246,"date":1248},{"_path":3313,"title":3314,"date":3316},{"_path":5141,"title":5142,"date":5144},{"_path":8113,"title":8114,"date":8115},"/posts/relational-vs-nosql-databases","Relational vs. NoSQL Databases: Why PostgreSQL Is My Default","2025-03-15T11:00:00.000Z",{"_path":8117,"title":1301,"date":8118},"/posts/hidden-cost-event-driven-architectures","2025-02-15T11:00:00.000Z",{"_path":8120,"title":8121,"date":8122},"/posts/gpt-sql-integration","Implementa un analizador de consultas en lenguaje natural para bases de datos SQL con GPT","2025-01-15T11:00:00.000Z",{"_path":8124,"title":8125,"date":8126},"/posts/java-security-best-practices","Java Security Best Practices: Preventing Vulnerabilities and Threats","2024-08-15T11:00:00.000Z",{"_path":8128,"title":8129,"date":8130},"/posts/circuit-breaking-resilience","Circuit Breaking in Distributed Systems","2024-07-15T11:00:00.000Z",{"_path":8132,"title":8133,"date":8134},"/posts/maven-and-github-actions","Automating Java Builds with Maven and GitHub Actions CI/CD","2024-06-15T11:00:00.000Z",{"_path":8136,"title":8137,"date":8138},"/posts/pattern-matching-sealed-classes-java21","A Guide to Pattern Matching and Sealed Classes in Java 21","2024-05-15T11:00:00.000Z",{"_path":8140,"title":8141,"date":8142},"/posts/async-java-completable-vs-reactive","Asynchronous Programming in Java: CompletableFuture vs Reactive Streams","2024-04-15T11:00:00.000Z",{"_path":8144,"title":8145,"date":8146},"/posts/mastering-concurrency","Mastering Concurrency in Java: Threads, Executors, and Virtual Threads","2024-03-15T11:00:00.000Z",{"_path":8148,"title":8149,"date":8150},"/posts/microservices-mistakes","Common Mistakes When Developing Microservices","2024-02-15T11:00:00.000Z",{"_path":8152,"title":8153,"date":8154},"/posts/tdd-testing","Test-Driven Development (TDD) in Java with JUnit and Mockito","2024-01-15T11:00:00.000Z",{"_path":8156,"title":8157,"date":8158},"/posts/single-table-design","Single Table Design","2023-12-27T11:00:00.000Z",{"_path":8160,"title":8161,"date":8162},"/posts/java-17","Exploring Java 17's New Features","2023-10-08T11:00:00.000Z",{"_path":8164,"title":8165,"date":8166},"/posts/hexagonal-architecture","Introduction to Hexagonal Architecture","2023-10-02 18:00:00",{"_path":8168,"title":8169,"date":8170},"/posts/terraform","Unleashing the Power of Infrastructure as Code (IaC)","2023-09-24 18:00:00",{"_path":8172,"title":8173,"date":8174},"/posts/python-virtual-environments","Python Virtual Environments (venv)","2023-09-17 22:00:00",{"_path":8176,"title":8177,"date":8178},"/posts/api-first-principles","API First principles","2023-09-01 22:15:18",{"_path":8180,"title":8181,"date":8182},"/posts/how-to-configure-ssh-key-based-authentication","How To Configure SSH Key-Based Authentication","2021-02-21 03:46:12",{"_path":8184,"title":8185,"date":8186},"/posts/algorithms-binary-search-tree","Algorithms - Binary Search Tree in kotlin","2020-12-06T11:00:00.000Z",{"_path":8188,"title":8189,"date":8186},"/posts/how-to-create-a-twitter-bot","How to create a Twitter bot",{"_path":8191,"title":8192,"date":8193},"/posts/manage-aws-infrastructure-with-terraform","Bootstrap complete Java application infrastructure in AWS with Terraform","2020-12-02T11:00:00.000Z",{"_path":8195,"title":8196,"date":8197},"/posts/algorithms-working-with-trees","Algorithms - Binary tree traversals in kotlin","2020-11-24T11:00:00.000Z",{"_path":8199,"title":8200,"date":8201},"/posts/playing-with-ocr-libraries","Playing with OCR libraries","2020-11-22T11:00:00.000Z",1791120245090]