[{"data":1,"prerenderedAt":5953},["ShallowReactive",2],{"post-spec-driven-development-openspec":3,"rel-spec-driven-development-openspec":1467,"sib-spec-driven-development-openspec":5850},{"_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":372,"_id":1462,"_source":1463,"_file":1464,"_stem":1465,"_extension":1466},"/posts/spec-driven-development-openspec","posts",false,"","Spec-Driven Development with OpenSpec: From Vague Request to Verifiable Change","A practical OpenSpec walkthrough: clarify an order-cancellation request, write behavior as scenarios, make design decisions, implement with AI, and revise the spec when reality changes.","post","2025-08-15T11:00:00.000Z","Turn an idea into behavior you can review, implement, and test","/assets/img/uploads/sdd-openspec.jpg","code",[14,16,17,18],"architecture","ai","testing","jaimedearcos",{"type":21,"children":22,"toc":1450},"root",[23,31,44,49,56,61,170,189,194,200,205,286,363,368,516,521,527,540,545,788,801,822,828,833,918,931,943,949,959,998,1003,1017,1023,1028,1068,1095,1108,1114,1134,1139,1178,1190,1196,1201,1300,1305,1352,1357,1377,1383,1396,1401,1407,1444],{"type":24,"tag":25,"props":26,"children":27},"element","p",{},[28],{"type":29,"value":30},"text","“Let customers cancel orders” sounds like a small feature. A developer or an AI coding agent could start writing an endpoint immediately. A few hours later, the code might compile and the happy-path test might pass. We could still disagree about what happens after shipment starts, whether a retry creates a second cancellation, or which customer is allowed to act.",{"type":24,"tag":25,"props":32,"children":33},{},[34,36,42],{"type":29,"value":35},"I use OpenSpec for this kind of conversation: make the intended behavior visible before implementation hides the unanswered questions. ",{"type":24,"tag":37,"props":38,"children":39},"strong",{},[40],{"type":29,"value":41},"Spec-driven development (SDD) is useful when the expensive mistake is building a plausible interpretation of an unclear request.",{"type":29,"value":43}," It does not remove judgment from the engineer. It gives people and AI agents something concrete to review, challenge, and verify.",{"type":24,"tag":25,"props":45,"children":46},{},[47],{"type":29,"value":48},"Let's follow one illustrative change from the first request to a finished implementation. The business rules below are example decisions for this walkthrough, not universal rules for order systems.",{"type":24,"tag":50,"props":51,"children":53},"h2",{"id":52},"the-request-is-not-yet-a-specification",[54],{"type":29,"value":55},"The request is not yet a specification",{"type":24,"tag":25,"props":57,"children":58},{},[59],{"type":29,"value":60},"Before opening an editor, ask what “cancel” means in this service:",{"type":24,"tag":62,"props":63,"children":64},"table",{},[65,84],{"type":24,"tag":66,"props":67,"children":68},"thead",{},[69],{"type":24,"tag":70,"props":71,"children":72},"tr",{},[73,79],{"type":24,"tag":74,"props":75,"children":76},"th",{},[77],{"type":29,"value":78},"Question",{"type":24,"tag":74,"props":80,"children":81},{},[82],{"type":29,"value":83},"A decision the team needs to make",{"type":24,"tag":85,"props":86,"children":87},"tbody",{},[88,102,123,136,149],{"type":24,"tag":70,"props":89,"children":90},{},[91,97],{"type":24,"tag":92,"props":93,"children":94},"td",{},[95],{"type":29,"value":96},"Who may cancel?",{"type":24,"tag":92,"props":98,"children":99},{},[100],{"type":29,"value":101},"The order owner, an administrator, or both?",{"type":24,"tag":70,"props":103,"children":104},{},[105,110],{"type":24,"tag":92,"props":106,"children":107},{},[108],{"type":29,"value":109},"Until when?",{"type":24,"tag":92,"props":111,"children":112},{},[113,115,121],{"type":29,"value":114},"Only while ",{"type":24,"tag":14,"props":116,"children":118},{"className":117},[],[119],{"type":29,"value":120},"PENDING",{"type":29,"value":122},", or also after payment and before shipment?",{"type":24,"tag":70,"props":124,"children":125},{},[126,131],{"type":24,"tag":92,"props":127,"children":128},{},[129],{"type":29,"value":130},"What does a retry do?",{"type":24,"tag":92,"props":132,"children":133},{},[134],{"type":29,"value":135},"Return the existing cancellation, or fail?",{"type":24,"tag":70,"props":137,"children":138},{},[139,144],{"type":24,"tag":92,"props":140,"children":141},{},[142],{"type":29,"value":143},"What if two requests race?",{"type":24,"tag":92,"props":145,"children":146},{},[147],{"type":29,"value":148},"Can both succeed without recording two transitions?",{"type":24,"tag":70,"props":150,"children":151},{},[152,157],{"type":24,"tag":92,"props":153,"children":154},{},[155],{"type":29,"value":156},"What happens downstream?",{"type":24,"tag":92,"props":158,"children":159},{},[160,162,168],{"type":29,"value":161},"Is an ",{"type":24,"tag":14,"props":163,"children":165},{"className":164},[],[166],{"type":29,"value":167},"OrderCancelled",{"type":29,"value":169}," event part of the contract?",{"type":24,"tag":25,"props":171,"children":172},{},[173,175,180,182,187],{"type":29,"value":174},"The list is not an excuse to delay work indefinitely. It is a way to find the decisions that would otherwise be made accidentally in a controller, an ORM mapping, or an agent's generated code. For this example, assume the team agrees that the authenticated owner may cancel a ",{"type":24,"tag":14,"props":176,"children":178},{"className":177},[],[179],{"type":29,"value":120},{"type":29,"value":181}," order; a shipped order cannot be cancelled; repeating a successful request returns the cancelled state; and one logical ",{"type":24,"tag":14,"props":183,"children":185},{"className":184},[],[186],{"type":29,"value":167},{"type":29,"value":188}," event is produced for the transition.",{"type":24,"tag":25,"props":190,"children":191},{},[192],{"type":29,"value":193},"The goal is now specific enough to propose a change.",{"type":24,"tag":50,"props":195,"children":197},{"id":196},"create-a-change-that-explains-why",[198],{"type":29,"value":199},"Create a change that explains why",{"type":24,"tag":25,"props":201,"children":202},{},[203],{"type":29,"value":204},"With OpenSpec initialized in the project, create a named change:",{"type":24,"tag":206,"props":207,"children":211},"pre",{"className":208,"code":209,"language":210,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark github-light","openspec new change add-order-cancellation \\\n  --goal \"Customers can cancel their pending orders safely\"\nopenspec status --change add-order-cancellation\n","bash",[212],{"type":24,"tag":14,"props":213,"children":214},{"__ignoreMap":7},[215,249,263],{"type":24,"tag":216,"props":217,"children":220},"span",{"class":218,"line":219},"line",1,[221,227,233,238,243],{"type":24,"tag":216,"props":222,"children":224},{"style":223},"--shiki-default:#B392F0;--shiki-light:#6F42C1",[225],{"type":29,"value":226},"openspec",{"type":24,"tag":216,"props":228,"children":230},{"style":229},"--shiki-default:#9ECBFF;--shiki-light:#032F62",[231],{"type":29,"value":232}," new",{"type":24,"tag":216,"props":234,"children":235},{"style":229},[236],{"type":29,"value":237}," change",{"type":24,"tag":216,"props":239,"children":240},{"style":229},[241],{"type":29,"value":242}," add-order-cancellation",{"type":24,"tag":216,"props":244,"children":246},{"style":245},"--shiki-default:#79B8FF;--shiki-light:#005CC5",[247],{"type":29,"value":248}," \\\n",{"type":24,"tag":216,"props":250,"children":252},{"class":218,"line":251},2,[253,258],{"type":24,"tag":216,"props":254,"children":255},{"style":245},[256],{"type":29,"value":257},"  --goal",{"type":24,"tag":216,"props":259,"children":260},{"style":229},[261],{"type":29,"value":262}," \"Customers can cancel their pending orders safely\"\n",{"type":24,"tag":216,"props":264,"children":266},{"class":218,"line":265},3,[267,271,276,281],{"type":24,"tag":216,"props":268,"children":269},{"style":223},[270],{"type":29,"value":226},{"type":24,"tag":216,"props":272,"children":273},{"style":229},[274],{"type":29,"value":275}," status",{"type":24,"tag":216,"props":277,"children":278},{"style":245},[279],{"type":29,"value":280}," --change",{"type":24,"tag":216,"props":282,"children":283},{"style":229},[284],{"type":29,"value":285}," add-order-cancellation\n",{"type":24,"tag":25,"props":287,"children":288},{},[289,291,297,299,305,307,313,315,320,322,327,329,335,337,342,344,350,352,361],{"type":29,"value":290},"The CLI creates ",{"type":24,"tag":14,"props":292,"children":294},{"className":293},[],[295],{"type":29,"value":296},"openspec/changes/add-order-cancellation/",{"type":29,"value":298},". In OpenSpec's default ",{"type":24,"tag":14,"props":300,"children":302},{"className":301},[],[303],{"type":29,"value":304},"spec-driven",{"type":29,"value":306}," workflow, ",{"type":24,"tag":14,"props":308,"children":310},{"className":309},[],[311],{"type":29,"value":312},"proposal.md",{"type":29,"value":314}," captures ",{"type":24,"tag":37,"props":316,"children":317},{},[318],{"type":29,"value":319},"why",{"type":29,"value":321},", a spec delta captures ",{"type":24,"tag":37,"props":323,"children":324},{},[325],{"type":29,"value":326},"what behavior changes",{"type":29,"value":328},", ",{"type":24,"tag":14,"props":330,"children":332},{"className":331},[],[333],{"type":29,"value":334},"design.md",{"type":29,"value":336}," records ",{"type":24,"tag":37,"props":338,"children":339},{},[340],{"type":29,"value":341},"how",{"type":29,"value":343}," where a design is needed, and ",{"type":24,"tag":14,"props":345,"children":347},{"className":346},[],[348],{"type":29,"value":349},"tasks.md",{"type":29,"value":351}," tracks implementation. Proposal comes first; spec and design can follow in either order; tasks depend on both. The ",{"type":24,"tag":353,"props":354,"children":358},"a",{"href":355,"rel":356},"https://openspec.dev/docs/schemas/spec-driven",[357],"nofollow",[359],{"type":29,"value":360},"schema reference",{"type":29,"value":362}," describes those roles.",{"type":24,"tag":25,"props":364,"children":365},{},[366],{"type":29,"value":367},"The proposal should make the problem and scope clear without pretending to be the implementation plan. A shortened example:",{"type":24,"tag":206,"props":369,"children":373},{"className":370,"code":371,"language":372,"meta":7,"style":7},"language-markdown shiki shiki-themes github-dark github-light","# Proposal\n\n## Why\nCustomers cannot cancel an eligible order without contacting support.\n\n## What Changes\n- Allow an authenticated owner to cancel a pending order.\n- Preserve a single logical cancellation when requests are repeated.\n- Publish the cancellation for downstream consumers.\n\n## Capabilities\n### New Capabilities\n- `order-cancellation`: Rules and outcomes of cancelling an order.\n\n## Impact\nOrder API, order persistence, cancellation event, and contract tests.\n","markdown",[374],{"type":24,"tag":14,"props":375,"children":376},{"__ignoreMap":7},[377,385,394,402,411,419,428,437,446,455,463,472,481,490,498,507],{"type":24,"tag":216,"props":378,"children":379},{"class":218,"line":219},[380],{"type":24,"tag":216,"props":381,"children":382},{},[383],{"type":29,"value":384},"# Proposal\n",{"type":24,"tag":216,"props":386,"children":387},{"class":218,"line":251},[388],{"type":24,"tag":216,"props":389,"children":391},{"emptyLinePlaceholder":390},true,[392],{"type":29,"value":393},"\n",{"type":24,"tag":216,"props":395,"children":396},{"class":218,"line":265},[397],{"type":24,"tag":216,"props":398,"children":399},{},[400],{"type":29,"value":401},"## Why\n",{"type":24,"tag":216,"props":403,"children":405},{"class":218,"line":404},4,[406],{"type":24,"tag":216,"props":407,"children":408},{},[409],{"type":29,"value":410},"Customers cannot cancel an eligible order without contacting support.\n",{"type":24,"tag":216,"props":412,"children":414},{"class":218,"line":413},5,[415],{"type":24,"tag":216,"props":416,"children":417},{"emptyLinePlaceholder":390},[418],{"type":29,"value":393},{"type":24,"tag":216,"props":420,"children":422},{"class":218,"line":421},6,[423],{"type":24,"tag":216,"props":424,"children":425},{},[426],{"type":29,"value":427},"## What Changes\n",{"type":24,"tag":216,"props":429,"children":431},{"class":218,"line":430},7,[432],{"type":24,"tag":216,"props":433,"children":434},{},[435],{"type":29,"value":436},"- Allow an authenticated owner to cancel a pending order.\n",{"type":24,"tag":216,"props":438,"children":440},{"class":218,"line":439},8,[441],{"type":24,"tag":216,"props":442,"children":443},{},[444],{"type":29,"value":445},"- Preserve a single logical cancellation when requests are repeated.\n",{"type":24,"tag":216,"props":447,"children":449},{"class":218,"line":448},9,[450],{"type":24,"tag":216,"props":451,"children":452},{},[453],{"type":29,"value":454},"- Publish the cancellation for downstream consumers.\n",{"type":24,"tag":216,"props":456,"children":458},{"class":218,"line":457},10,[459],{"type":24,"tag":216,"props":460,"children":461},{"emptyLinePlaceholder":390},[462],{"type":29,"value":393},{"type":24,"tag":216,"props":464,"children":466},{"class":218,"line":465},11,[467],{"type":24,"tag":216,"props":468,"children":469},{},[470],{"type":29,"value":471},"## Capabilities\n",{"type":24,"tag":216,"props":473,"children":475},{"class":218,"line":474},12,[476],{"type":24,"tag":216,"props":477,"children":478},{},[479],{"type":29,"value":480},"### New Capabilities\n",{"type":24,"tag":216,"props":482,"children":484},{"class":218,"line":483},13,[485],{"type":24,"tag":216,"props":486,"children":487},{},[488],{"type":29,"value":489},"- `order-cancellation`: Rules and outcomes of cancelling an order.\n",{"type":24,"tag":216,"props":491,"children":493},{"class":218,"line":492},14,[494],{"type":24,"tag":216,"props":495,"children":496},{"emptyLinePlaceholder":390},[497],{"type":29,"value":393},{"type":24,"tag":216,"props":499,"children":501},{"class":218,"line":500},15,[502],{"type":24,"tag":216,"props":503,"children":504},{},[505],{"type":29,"value":506},"## Impact\n",{"type":24,"tag":216,"props":508,"children":510},{"class":218,"line":509},16,[511],{"type":24,"tag":216,"props":512,"children":513},{},[514],{"type":29,"value":515},"Order API, order persistence, cancellation event, and contract tests.\n",{"type":24,"tag":25,"props":517,"children":518},{},[519],{"type":29,"value":520},"At review time, the most useful question is whether this is the right change. If the request also asks for refunds, inventory release, and cancelling shipments, the proposal needs to state which of those are in scope and which require another change. A small, named unit of work is easier to understand and integrate.",{"type":24,"tag":50,"props":522,"children":524},{"id":523},"put-observable-behavior-in-the-spec",[525],{"type":29,"value":526},"Put observable behavior in the spec",{"type":24,"tag":25,"props":528,"children":529},{},[530,532,538],{"type":29,"value":531},"The delta spec belongs under the capability named in the proposal, for example ",{"type":24,"tag":14,"props":533,"children":535},{"className":534},[],[536],{"type":29,"value":537},"openspec/changes/add-order-cancellation/specs/order-cancellation/spec.md",{"type":29,"value":539},". It describes the behavior that changes, not the classes or SQL statements that happen to implement it.",{"type":24,"tag":25,"props":541,"children":542},{},[543],{"type":29,"value":544},"Here is an illustrative excerpt in OpenSpec's requirement-and-scenario format:",{"type":24,"tag":206,"props":546,"children":548},{"className":370,"code":547,"language":372,"meta":7,"style":7},"# Spec Delta\n\n## Purpose\nDefine when the owner of an order may cancel it and how the service\nresponds to rejected or repeated cancellation requests.\n\n## ADDED Requirements\n\n### Requirement: Cancel an eligible order\nThe service MUST allow the authenticated owner to cancel a PENDING order.\n\n#### Scenario: Eligible order\n- **WHEN** the owner requests cancellation of a PENDING order\n- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded\n\n#### Scenario: Shipment has started\n- **WHEN** the owner requests cancellation of a SHIPPED order\n- **THEN** the service rejects the request and leaves the order unchanged\n\n#### Scenario: Not the owner\n- **WHEN** a different authenticated customer requests cancellation\n- **THEN** the service rejects the request and leaves the order unchanged\n\n### Requirement: Repeat a completed cancellation\nThe service MUST handle repeated requests without creating a second cancellation.\n\n#### Scenario: Repeated request\n- **WHEN** the owner repeats a successful cancellation request\n- **THEN** the service returns the existing CANCELLED state without a new transition\n",[549],{"type":24,"tag":14,"props":550,"children":551},{"__ignoreMap":7},[552,560,567,575,583,591,598,606,613,621,629,636,644,652,660,667,675,684,693,701,710,719,727,735,744,753,761,770,779],{"type":24,"tag":216,"props":553,"children":554},{"class":218,"line":219},[555],{"type":24,"tag":216,"props":556,"children":557},{},[558],{"type":29,"value":559},"# Spec Delta\n",{"type":24,"tag":216,"props":561,"children":562},{"class":218,"line":251},[563],{"type":24,"tag":216,"props":564,"children":565},{"emptyLinePlaceholder":390},[566],{"type":29,"value":393},{"type":24,"tag":216,"props":568,"children":569},{"class":218,"line":265},[570],{"type":24,"tag":216,"props":571,"children":572},{},[573],{"type":29,"value":574},"## Purpose\n",{"type":24,"tag":216,"props":576,"children":577},{"class":218,"line":404},[578],{"type":24,"tag":216,"props":579,"children":580},{},[581],{"type":29,"value":582},"Define when the owner of an order may cancel it and how the service\n",{"type":24,"tag":216,"props":584,"children":585},{"class":218,"line":413},[586],{"type":24,"tag":216,"props":587,"children":588},{},[589],{"type":29,"value":590},"responds to rejected or repeated cancellation requests.\n",{"type":24,"tag":216,"props":592,"children":593},{"class":218,"line":421},[594],{"type":24,"tag":216,"props":595,"children":596},{"emptyLinePlaceholder":390},[597],{"type":29,"value":393},{"type":24,"tag":216,"props":599,"children":600},{"class":218,"line":430},[601],{"type":24,"tag":216,"props":602,"children":603},{},[604],{"type":29,"value":605},"## ADDED Requirements\n",{"type":24,"tag":216,"props":607,"children":608},{"class":218,"line":439},[609],{"type":24,"tag":216,"props":610,"children":611},{"emptyLinePlaceholder":390},[612],{"type":29,"value":393},{"type":24,"tag":216,"props":614,"children":615},{"class":218,"line":448},[616],{"type":24,"tag":216,"props":617,"children":618},{},[619],{"type":29,"value":620},"### Requirement: Cancel an eligible order\n",{"type":24,"tag":216,"props":622,"children":623},{"class":218,"line":457},[624],{"type":24,"tag":216,"props":625,"children":626},{},[627],{"type":29,"value":628},"The service MUST allow the authenticated owner to cancel a PENDING order.\n",{"type":24,"tag":216,"props":630,"children":631},{"class":218,"line":465},[632],{"type":24,"tag":216,"props":633,"children":634},{"emptyLinePlaceholder":390},[635],{"type":29,"value":393},{"type":24,"tag":216,"props":637,"children":638},{"class":218,"line":474},[639],{"type":24,"tag":216,"props":640,"children":641},{},[642],{"type":29,"value":643},"#### Scenario: Eligible order\n",{"type":24,"tag":216,"props":645,"children":646},{"class":218,"line":483},[647],{"type":24,"tag":216,"props":648,"children":649},{},[650],{"type":29,"value":651},"- **WHEN** the owner requests cancellation of a PENDING order\n",{"type":24,"tag":216,"props":653,"children":654},{"class":218,"line":492},[655],{"type":24,"tag":216,"props":656,"children":657},{},[658],{"type":29,"value":659},"- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded\n",{"type":24,"tag":216,"props":661,"children":662},{"class":218,"line":500},[663],{"type":24,"tag":216,"props":664,"children":665},{"emptyLinePlaceholder":390},[666],{"type":29,"value":393},{"type":24,"tag":216,"props":668,"children":669},{"class":218,"line":509},[670],{"type":24,"tag":216,"props":671,"children":672},{},[673],{"type":29,"value":674},"#### Scenario: Shipment has started\n",{"type":24,"tag":216,"props":676,"children":678},{"class":218,"line":677},17,[679],{"type":24,"tag":216,"props":680,"children":681},{},[682],{"type":29,"value":683},"- **WHEN** the owner requests cancellation of a SHIPPED order\n",{"type":24,"tag":216,"props":685,"children":687},{"class":218,"line":686},18,[688],{"type":24,"tag":216,"props":689,"children":690},{},[691],{"type":29,"value":692},"- **THEN** the service rejects the request and leaves the order unchanged\n",{"type":24,"tag":216,"props":694,"children":696},{"class":218,"line":695},19,[697],{"type":24,"tag":216,"props":698,"children":699},{"emptyLinePlaceholder":390},[700],{"type":29,"value":393},{"type":24,"tag":216,"props":702,"children":704},{"class":218,"line":703},20,[705],{"type":24,"tag":216,"props":706,"children":707},{},[708],{"type":29,"value":709},"#### Scenario: Not the owner\n",{"type":24,"tag":216,"props":711,"children":713},{"class":218,"line":712},21,[714],{"type":24,"tag":216,"props":715,"children":716},{},[717],{"type":29,"value":718},"- **WHEN** a different authenticated customer requests cancellation\n",{"type":24,"tag":216,"props":720,"children":722},{"class":218,"line":721},22,[723],{"type":24,"tag":216,"props":724,"children":725},{},[726],{"type":29,"value":692},{"type":24,"tag":216,"props":728,"children":730},{"class":218,"line":729},23,[731],{"type":24,"tag":216,"props":732,"children":733},{"emptyLinePlaceholder":390},[734],{"type":29,"value":393},{"type":24,"tag":216,"props":736,"children":738},{"class":218,"line":737},24,[739],{"type":24,"tag":216,"props":740,"children":741},{},[742],{"type":29,"value":743},"### Requirement: Repeat a completed cancellation\n",{"type":24,"tag":216,"props":745,"children":747},{"class":218,"line":746},25,[748],{"type":24,"tag":216,"props":749,"children":750},{},[751],{"type":29,"value":752},"The service MUST handle repeated requests without creating a second cancellation.\n",{"type":24,"tag":216,"props":754,"children":756},{"class":218,"line":755},26,[757],{"type":24,"tag":216,"props":758,"children":759},{"emptyLinePlaceholder":390},[760],{"type":29,"value":393},{"type":24,"tag":216,"props":762,"children":764},{"class":218,"line":763},27,[765],{"type":24,"tag":216,"props":766,"children":767},{},[768],{"type":29,"value":769},"#### Scenario: Repeated request\n",{"type":24,"tag":216,"props":771,"children":773},{"class":218,"line":772},28,[774],{"type":24,"tag":216,"props":775,"children":776},{},[777],{"type":29,"value":778},"- **WHEN** the owner repeats a successful cancellation request\n",{"type":24,"tag":216,"props":780,"children":782},{"class":218,"line":781},29,[783],{"type":24,"tag":216,"props":784,"children":785},{},[786],{"type":29,"value":787},"- **THEN** the service returns the existing CANCELLED state without a new transition\n",{"type":24,"tag":25,"props":789,"children":790},{},[791,793,799],{"type":29,"value":792},"This still leaves questions for the API contract: should a shipped order return ",{"type":24,"tag":14,"props":794,"children":796},{"className":795},[],[797],{"type":29,"value":798},"409 Conflict",{"type":29,"value":800},"? What should an unauthenticated caller see? Those decisions should be made explicitly and added to the relevant spec or existing API contract before implementation. A spec is useful because its gaps are visible to a reviewer.",{"type":24,"tag":25,"props":802,"children":803},{},[804,806,812,814,820],{"type":29,"value":805},"Keep the wording at the level of behavior. “Use optimistic locking in ",{"type":24,"tag":14,"props":807,"children":809},{"className":808},[],[810],{"type":29,"value":811},"OrderRepository",{"type":29,"value":813},"” is a possible design decision, not a customer-facing requirement. If the implementation can switch from one concurrency mechanism to another without changing the promise, the mechanism belongs in the design. OpenSpec's ",{"type":24,"tag":353,"props":815,"children":817},{"href":355,"rel":816},[357],[818],{"type":29,"value":819},"spec guidance",{"type":29,"value":821}," makes the same distinction.",{"type":24,"tag":50,"props":823,"children":825},{"id":824},"use-the-design-for-technical-trade-offs",[826],{"type":29,"value":827},"Use the design for technical trade-offs",{"type":24,"tag":25,"props":829,"children":830},{},[831],{"type":29,"value":832},"The design connects those requirements to the actual system. For this example, it might record:",{"type":24,"tag":206,"props":834,"children":836},{"className":370,"code":835,"language":372,"meta":7,"style":7},"# Design\n\n## Decisions\n- Use an atomic conditional update to change PENDING to CANCELLED.\n- Record the OrderCancelled event in the same database transaction.\n- On a lost race, reload the order and return its current state when it is CANCELLED.\n\n## Trade-offs\n- A conditional update avoids holding a database lock across the request.\n- The event must be durable even if the process stops after commit.\n",[837],{"type":24,"tag":14,"props":838,"children":839},{"__ignoreMap":7},[840,848,855,863,871,879,887,894,902,910],{"type":24,"tag":216,"props":841,"children":842},{"class":218,"line":219},[843],{"type":24,"tag":216,"props":844,"children":845},{},[846],{"type":29,"value":847},"# Design\n",{"type":24,"tag":216,"props":849,"children":850},{"class":218,"line":251},[851],{"type":24,"tag":216,"props":852,"children":853},{"emptyLinePlaceholder":390},[854],{"type":29,"value":393},{"type":24,"tag":216,"props":856,"children":857},{"class":218,"line":265},[858],{"type":24,"tag":216,"props":859,"children":860},{},[861],{"type":29,"value":862},"## Decisions\n",{"type":24,"tag":216,"props":864,"children":865},{"class":218,"line":404},[866],{"type":24,"tag":216,"props":867,"children":868},{},[869],{"type":29,"value":870},"- Use an atomic conditional update to change PENDING to CANCELLED.\n",{"type":24,"tag":216,"props":872,"children":873},{"class":218,"line":413},[874],{"type":24,"tag":216,"props":875,"children":876},{},[877],{"type":29,"value":878},"- Record the OrderCancelled event in the same database transaction.\n",{"type":24,"tag":216,"props":880,"children":881},{"class":218,"line":421},[882],{"type":24,"tag":216,"props":883,"children":884},{},[885],{"type":29,"value":886},"- On a lost race, reload the order and return its current state when it is CANCELLED.\n",{"type":24,"tag":216,"props":888,"children":889},{"class":218,"line":430},[890],{"type":24,"tag":216,"props":891,"children":892},{"emptyLinePlaceholder":390},[893],{"type":29,"value":393},{"type":24,"tag":216,"props":895,"children":896},{"class":218,"line":439},[897],{"type":24,"tag":216,"props":898,"children":899},{},[900],{"type":29,"value":901},"## Trade-offs\n",{"type":24,"tag":216,"props":903,"children":904},{"class":218,"line":448},[905],{"type":24,"tag":216,"props":906,"children":907},{},[908],{"type":29,"value":909},"- A conditional update avoids holding a database lock across the request.\n",{"type":24,"tag":216,"props":911,"children":912},{"class":218,"line":457},[913],{"type":24,"tag":216,"props":914,"children":915},{},[916],{"type":29,"value":917},"- The event must be durable even if the process stops after commit.\n",{"type":24,"tag":25,"props":919,"children":920},{},[921,923,929],{"type":29,"value":922},"That is a sketch, not production-ready code. It shows where the technical reasoning lives. A database lock could be valid instead; the design should explain why the chosen approach fits this service. If the event is required for downstream consistency, the ",{"type":24,"tag":353,"props":924,"children":926},{"href":925},"/transactional-outbox/",[927],{"type":29,"value":928},"Transactional Outbox",{"type":29,"value":930}," pattern is one way to make state and publication intent atomic.",{"type":24,"tag":25,"props":932,"children":933},{},[934,936,941],{"type":29,"value":935},"Design is not compulsory paperwork for every change. OpenSpec can omit ",{"type":24,"tag":14,"props":937,"children":939},{"className":938},[],[940],{"type":29,"value":334},{"type":29,"value":942}," when the change does not need a separate technical decision. It becomes valuable when the choice has consequences for concurrency, migrations, security, dependencies, or recovery.",{"type":24,"tag":50,"props":944,"children":946},{"id":945},"turn-the-plan-into-work-you-can-verify",[947],{"type":29,"value":948},"Turn the plan into work you can verify",{"type":24,"tag":25,"props":950,"children":951},{},[952,957],{"type":24,"tag":14,"props":953,"children":955},{"className":954},[],[956],{"type":29,"value":349},{"type":29,"value":958}," turns the spec and design into an implementation checklist. Each item should end in an observable result:",{"type":24,"tag":206,"props":960,"children":962},{"className":370,"code":961,"language":372,"meta":7,"style":7},"- [ ] Add the cancellation API and authorization checks.\n- [ ] Persist the order transition and event intent atomically.\n- [ ] Test eligible, shipped, unauthorized, and repeated requests.\n- [ ] Run the project checks and review the API response contract.\n",[963],{"type":24,"tag":14,"props":964,"children":965},{"__ignoreMap":7},[966,974,982,990],{"type":24,"tag":216,"props":967,"children":968},{"class":218,"line":219},[969],{"type":24,"tag":216,"props":970,"children":971},{},[972],{"type":29,"value":973},"- [ ] Add the cancellation API and authorization checks.\n",{"type":24,"tag":216,"props":975,"children":976},{"class":218,"line":251},[977],{"type":24,"tag":216,"props":978,"children":979},{},[980],{"type":29,"value":981},"- [ ] Persist the order transition and event intent atomically.\n",{"type":24,"tag":216,"props":983,"children":984},{"class":218,"line":265},[985],{"type":24,"tag":216,"props":986,"children":987},{},[988],{"type":29,"value":989},"- [ ] Test eligible, shipped, unauthorized, and repeated requests.\n",{"type":24,"tag":216,"props":991,"children":992},{"class":218,"line":404},[993],{"type":24,"tag":216,"props":994,"children":995},{},[996],{"type":29,"value":997},"- [ ] Run the project checks and review the API response contract.\n",{"type":24,"tag":25,"props":999,"children":1000},{},[1001],{"type":29,"value":1002},"The spec says what must be true. The tasks say what work remains. Tests provide evidence that the implementation behaves as promised. None of these files replaces the others.",{"type":24,"tag":25,"props":1004,"children":1005},{},[1006,1008,1015],{"type":29,"value":1007},"Before code starts, review the three questions OpenSpec's ",{"type":24,"tag":353,"props":1009,"children":1012},{"href":1010,"rel":1011},"https://openspec.dev/docs/quickstart",[357],[1013],{"type":29,"value":1014},"quickstart",{"type":29,"value":1016}," emphasizes: Is the proposal the right problem and size? Would you accept the spec's scenarios as done? Do the tasks cover those scenarios without adding unrelated work? Fixing a sentence now is cheaper than reviewing a large implementation of the wrong rule.",{"type":24,"tag":50,"props":1018,"children":1020},{"id":1019},"give-the-agent-a-bounded-handoff",[1021],{"type":29,"value":1022},"Give the agent a bounded handoff",{"type":24,"tag":25,"props":1024,"children":1025},{},[1026],{"type":29,"value":1027},"Once the artifacts are agreed, the implementation request can be short:",{"type":24,"tag":206,"props":1029,"children":1032},{"className":1030,"code":1031,"language":29,"meta":7,"style":7},"language-text shiki shiki-themes github-dark github-light","Apply the add-order-cancellation change. Read its proposal, spec,\ndesign, and tasks before editing code. Implement one task at a time.\nRun the relevant tests and report any requirement that is ambiguous\nor cannot be met by the current design. Do not invent a new business rule.\n",[1033],{"type":24,"tag":14,"props":1034,"children":1035},{"__ignoreMap":7},[1036,1044,1052,1060],{"type":24,"tag":216,"props":1037,"children":1038},{"class":218,"line":219},[1039],{"type":24,"tag":216,"props":1040,"children":1041},{},[1042],{"type":29,"value":1043},"Apply the add-order-cancellation change. Read its proposal, spec,\n",{"type":24,"tag":216,"props":1045,"children":1046},{"class":218,"line":251},[1047],{"type":24,"tag":216,"props":1048,"children":1049},{},[1050],{"type":29,"value":1051},"design, and tasks before editing code. Implement one task at a time.\n",{"type":24,"tag":216,"props":1053,"children":1054},{"class":218,"line":265},[1055],{"type":24,"tag":216,"props":1056,"children":1057},{},[1058],{"type":29,"value":1059},"Run the relevant tests and report any requirement that is ambiguous\n",{"type":24,"tag":216,"props":1061,"children":1062},{"class":218,"line":404},[1063],{"type":24,"tag":216,"props":1064,"children":1065},{},[1066],{"type":29,"value":1067},"or cannot be met by the current design. Do not invent a new business rule.\n",{"type":24,"tag":25,"props":1069,"children":1070},{},[1071,1073,1078,1080,1085,1087,1093],{"type":29,"value":1072},"OpenSpec calls this the ",{"type":24,"tag":37,"props":1074,"children":1075},{},[1076],{"type":29,"value":1077},"apply",{"type":29,"value":1079}," phase. It tracks progress in the checkboxes in ",{"type":24,"tag":14,"props":1081,"children":1083},{"className":1082},[],[1084],{"type":29,"value":349},{"type":29,"value":1086},"; it is not a promise that the CLI has turned the spec into correct code. The ",{"type":24,"tag":353,"props":1088,"children":1090},{"href":1010,"rel":1089},[357],[1091],{"type":29,"value":1092},"OpenSpec workflow",{"type":29,"value":1094}," explicitly allows the artifacts to be corrected while work is underway.",{"type":24,"tag":25,"props":1096,"children":1097},{},[1098,1100,1106],{"type":29,"value":1099},"This is particularly useful with AI agents. A new session can read the change and the remaining tasks instead of relying on an old chat transcript. The agent still needs access to the actual codebase and its tests, and a person still needs to review behavior at the boundary of the change. If several agents work in parallel, give each a separate ",{"type":24,"tag":353,"props":1101,"children":1103},{"href":1102},"/git-worktrees-parallel-development/",[1104],{"type":29,"value":1105},"Git worktree",{"type":29,"value":1107}," and keep ownership of integration clear.",{"type":24,"tag":50,"props":1109,"children":1111},{"id":1110},"when-implementation-reveals-a-missing-scenario",[1112],{"type":29,"value":1113},"When implementation reveals a missing scenario",{"type":24,"tag":25,"props":1115,"children":1116},{},[1117,1119,1124,1126,1132],{"type":29,"value":1118},"Suppose a test exposes a race: two cancellation requests read the same ",{"type":24,"tag":14,"props":1120,"children":1122},{"className":1121},[],[1123],{"type":29,"value":120},{"type":29,"value":1125}," order before either writes. The original repeated-request scenario covers a request made ",{"type":24,"tag":1127,"props":1128,"children":1129},"em",{},[1130],{"type":29,"value":1131},"after",{"type":29,"value":1133}," cancellation, but does not say what simultaneous requests should do.",{"type":24,"tag":25,"props":1135,"children":1136},{},[1137],{"type":29,"value":1138},"Do not silently choose the easiest behavior in code. Add the missing scenario to the spec:",{"type":24,"tag":206,"props":1140,"children":1142},{"className":370,"code":1141,"language":372,"meta":7,"style":7},"#### Scenario: Concurrent cancellation requests\n- **WHEN** two requests from the owner try to cancel the same PENDING order\n- **THEN** exactly one cancellation transition and one logical event are recorded\n- **AND** both requests can observe the final CANCELLED state\n",[1143],{"type":24,"tag":14,"props":1144,"children":1145},{"__ignoreMap":7},[1146,1154,1162,1170],{"type":24,"tag":216,"props":1147,"children":1148},{"class":218,"line":219},[1149],{"type":24,"tag":216,"props":1150,"children":1151},{},[1152],{"type":29,"value":1153},"#### Scenario: Concurrent cancellation requests\n",{"type":24,"tag":216,"props":1155,"children":1156},{"class":218,"line":251},[1157],{"type":24,"tag":216,"props":1158,"children":1159},{},[1160],{"type":29,"value":1161},"- **WHEN** two requests from the owner try to cancel the same PENDING order\n",{"type":24,"tag":216,"props":1163,"children":1164},{"class":218,"line":265},[1165],{"type":24,"tag":216,"props":1166,"children":1167},{},[1168],{"type":29,"value":1169},"- **THEN** exactly one cancellation transition and one logical event are recorded\n",{"type":24,"tag":216,"props":1171,"children":1172},{"class":218,"line":404},[1173],{"type":24,"tag":216,"props":1174,"children":1175},{},[1176],{"type":29,"value":1177},"- **AND** both requests can observe the final CANCELLED state\n",{"type":24,"tag":25,"props":1179,"children":1180},{},[1181,1183,1188],{"type":29,"value":1182},"Then revisit the design and tasks. The conditional update may let one request win and the other load the newly cancelled row; add a concurrency test that actually runs both requests. If the chosen API semantics require one request to report a conflict instead, record that decision in the spec and test it. The point is that ",{"type":24,"tag":37,"props":1184,"children":1185},{},[1186],{"type":29,"value":1187},"the spec changes when understanding changes",{"type":29,"value":1189},". It should describe the behavior the team intends to ship, not preserve an early guess for appearances.",{"type":24,"tag":50,"props":1191,"children":1193},{"id":1192},"verify-the-implementation-then-archive-the-change",[1194],{"type":29,"value":1195},"Verify the implementation, then archive the change",{"type":24,"tag":25,"props":1197,"children":1198},{},[1199],{"type":29,"value":1200},"The final review needs evidence for each important scenario:",{"type":24,"tag":62,"props":1202,"children":1203},{},[1204,1220],{"type":24,"tag":66,"props":1205,"children":1206},{},[1207],{"type":24,"tag":70,"props":1208,"children":1209},{},[1210,1215],{"type":24,"tag":74,"props":1211,"children":1212},{},[1213],{"type":29,"value":1214},"Scenario",{"type":24,"tag":74,"props":1216,"children":1217},{},[1218],{"type":29,"value":1219},"Useful evidence",{"type":24,"tag":85,"props":1221,"children":1222},{},[1223,1236,1249,1261,1274,1287],{"type":24,"tag":70,"props":1224,"children":1225},{},[1226,1231],{"type":24,"tag":92,"props":1227,"children":1228},{},[1229],{"type":29,"value":1230},"Eligible owner",{"type":24,"tag":92,"props":1232,"children":1233},{},[1234],{"type":29,"value":1235},"API test confirms the state transition and response",{"type":24,"tag":70,"props":1237,"children":1238},{},[1239,1244],{"type":24,"tag":92,"props":1240,"children":1241},{},[1242],{"type":29,"value":1243},"Shipped order",{"type":24,"tag":92,"props":1245,"children":1246},{},[1247],{"type":29,"value":1248},"API test confirms rejection and unchanged state",{"type":24,"tag":70,"props":1250,"children":1251},{},[1252,1257],{"type":24,"tag":92,"props":1253,"children":1254},{},[1255],{"type":29,"value":1256},"Different customer",{"type":24,"tag":92,"props":1258,"children":1259},{},[1260],{"type":29,"value":1248},{"type":24,"tag":70,"props":1262,"children":1263},{},[1264,1269],{"type":24,"tag":92,"props":1265,"children":1266},{},[1267],{"type":29,"value":1268},"Repeated request",{"type":24,"tag":92,"props":1270,"children":1271},{},[1272],{"type":29,"value":1273},"Test confirms no second logical transition or event",{"type":24,"tag":70,"props":1275,"children":1276},{},[1277,1282],{"type":24,"tag":92,"props":1278,"children":1279},{},[1280],{"type":29,"value":1281},"Concurrent requests",{"type":24,"tag":92,"props":1283,"children":1284},{},[1285],{"type":29,"value":1286},"Integration test confirms the chosen race behavior",{"type":24,"tag":70,"props":1288,"children":1289},{},[1290,1295],{"type":24,"tag":92,"props":1291,"children":1292},{},[1293],{"type":29,"value":1294},"Downstream event",{"type":24,"tag":92,"props":1296,"children":1297},{},[1298],{"type":29,"value":1299},"Test confirms the event intent commits with the order change",{"type":24,"tag":25,"props":1301,"children":1302},{},[1303],{"type":29,"value":1304},"Run OpenSpec's structural validation too:",{"type":24,"tag":206,"props":1306,"children":1308},{"className":208,"code":1307,"language":210,"meta":7,"style":7},"openspec validate add-order-cancellation --strict\nopenspec status --change add-order-cancellation\n",[1309],{"type":24,"tag":14,"props":1310,"children":1311},{"__ignoreMap":7},[1312,1333],{"type":24,"tag":216,"props":1313,"children":1314},{"class":218,"line":219},[1315,1319,1324,1328],{"type":24,"tag":216,"props":1316,"children":1317},{"style":223},[1318],{"type":29,"value":226},{"type":24,"tag":216,"props":1320,"children":1321},{"style":229},[1322],{"type":29,"value":1323}," validate",{"type":24,"tag":216,"props":1325,"children":1326},{"style":229},[1327],{"type":29,"value":242},{"type":24,"tag":216,"props":1329,"children":1330},{"style":245},[1331],{"type":29,"value":1332}," --strict\n",{"type":24,"tag":216,"props":1334,"children":1335},{"class":218,"line":251},[1336,1340,1344,1348],{"type":24,"tag":216,"props":1337,"children":1338},{"style":223},[1339],{"type":29,"value":226},{"type":24,"tag":216,"props":1341,"children":1342},{"style":229},[1343],{"type":29,"value":275},{"type":24,"tag":216,"props":1345,"children":1346},{"style":245},[1347],{"type":29,"value":280},{"type":24,"tag":216,"props":1349,"children":1350},{"style":229},[1351],{"type":29,"value":285},{"type":24,"tag":25,"props":1353,"children":1354},{},[1355],{"type":29,"value":1356},"Validation checks the change artifacts; it cannot prove that the service meets its requirements. Run the project's tests, inspect the diff, and compare the result with the spec. If code and spec disagree, decide which is wrong before declaring the task complete.",{"type":24,"tag":25,"props":1358,"children":1359},{},[1360,1362,1368,1370,1375],{"type":29,"value":1361},"When the implementation and checklist are complete, archive the change. For a behavior-changing capability, OpenSpec moves the change folder to ",{"type":24,"tag":14,"props":1363,"children":1365},{"className":1364},[],[1366],{"type":29,"value":1367},"openspec/changes/archive/",{"type":29,"value":1369}," and merges the delta requirements into the main specs, so they describe the system as built. The ",{"type":24,"tag":353,"props":1371,"children":1373},{"href":1010,"rel":1372},[357],[1374],{"type":29,"value":1014},{"type":29,"value":1376}," walks through this step. Use your team's normal Git review and integration process around it; OpenSpec does not replace that process.",{"type":24,"tag":50,"props":1378,"children":1380},{"id":1379},"keep-the-method-proportional-to-the-change",[1381],{"type":29,"value":1382},"Keep the method proportional to the change",{"type":24,"tag":25,"props":1384,"children":1385},{},[1386,1388,1394],{"type":29,"value":1387},"SDD earns its keep when an unclear decision would be expensive: public API behavior, cross-service workflows, state transitions, migrations, or security rules. It is lighter when you keep each change small and write only the artifacts it needs. A typo fix does not require an invented business requirement. OpenSpec supports ",{"type":24,"tag":14,"props":1389,"children":1391},{"className":1390},[],[1392],{"type":29,"value":1393},"skip_specs: true",{"type":29,"value":1395}," for changes with no spec-level behavior change, such as documentation or some tooling work, and allows a design to be omitted when no design decision warrants it.",{"type":24,"tag":25,"props":1397,"children":1398},{},[1399],{"type":29,"value":1400},"For me, the value is not producing more Markdown. It is discovering disagreement while the change is still easy to reshape, and giving implementation and review a shared definition of done. An AI agent can accelerate the code. A good spec makes it easier to tell whether that acceleration took us in the right direction.",{"type":24,"tag":50,"props":1402,"children":1404},{"id":1403},"further-reading",[1405],{"type":29,"value":1406},"Further reading",{"type":24,"tag":1408,"props":1409,"children":1410},"ul",{},[1411,1423,1433],{"type":24,"tag":1412,"props":1413,"children":1414},"li",{},[1415,1421],{"type":24,"tag":353,"props":1416,"children":1418},{"href":1010,"rel":1417},[357],[1419],{"type":29,"value":1420},"OpenSpec: Quickstart",{"type":29,"value":1422},".",{"type":24,"tag":1412,"props":1424,"children":1425},{},[1426,1432],{"type":24,"tag":353,"props":1427,"children":1429},{"href":355,"rel":1428},[357],[1430],{"type":29,"value":1431},"OpenSpec: spec-driven schema",{"type":29,"value":1422},{"type":24,"tag":1412,"props":1434,"children":1435},{},[1436,1443],{"type":24,"tag":353,"props":1437,"children":1440},{"href":1438,"rel":1439},"https://openspec.dev/docs/cli",[357],[1441],{"type":29,"value":1442},"OpenSpec: CLI reference",{"type":29,"value":1422},{"type":24,"tag":1445,"props":1446,"children":1447},"style",{},[1448],{"type":29,"value":1449},"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":251,"depth":251,"links":1451},[1452,1453,1454,1455,1456,1457,1458,1459,1460,1461],{"id":52,"depth":251,"text":55},{"id":196,"depth":251,"text":199},{"id":523,"depth":251,"text":526},{"id":824,"depth":251,"text":827},{"id":945,"depth":251,"text":948},{"id":1019,"depth":251,"text":1022},{"id":1110,"depth":251,"text":1113},{"id":1192,"depth":251,"text":1195},{"id":1379,"depth":251,"text":1382},{"id":1403,"depth":251,"text":1406},"content:posts:spec-driven-development-openspec.md","content","posts/spec-driven-development-openspec.md","posts/spec-driven-development-openspec","md",[1468,2612,3800],{"_path":1469,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1470,"description":1471,"layout":10,"date":1472,"subtitle":1473,"image":1474,"optimized_image":1474,"category":14,"tags":1475,"author":19,"paginate":6,"body":1477,"_type":372,"_id":2609,"_source":1463,"_file":2610,"_stem":2611,"_extension":1466},"/posts/ai-development-workspace-opencode-openspec","Build Your AI Development Workspace with OpenCode and OpenSpec","A practical introduction to OpenCode, OpenSpec, AGENTS.md, custom agents, and skills, using one small API change to show what each piece does.","2026-10-06T05:00:00.000Z","Give an AI coding agent context, a clear change, and reusable ways to work","/assets/img/uploads/ai-development-workspace.jpg",[14,17,16,1476],"best practices",{"type":21,"children":1478,"toc":2602},[1479,1491,1522,1640,1660,1666,1679,1730,1742,1771,1833,1846,1852,1857,1871,1920,1950,1963,1977,1982,2021,2046,2131,2143,2149,2163,2176,2277,2307,2312,2318,2337,2348,2477,2507,2519,2525,2530,2588,2593,2598],{"type":24,"tag":25,"props":1480,"children":1481},{},[1482,1484,1489],{"type":29,"value":1483},"An AI coding agent can edit files and run tests within minutes. That speed is useful, but it also makes a familiar mistake faster: implementing a reasonable answer to a question nobody has properly defined. The agent may know the language and framework while knowing very little about ",{"type":24,"tag":1127,"props":1485,"children":1486},{},[1487],{"type":29,"value":1488},"your",{"type":29,"value":1490}," repository, the behavior you promised users, or the checks your team expects before a change is done.",{"type":24,"tag":25,"props":1492,"children":1493},{},[1494,1496,1503,1505,1511,1513,1520],{"type":29,"value":1495},"My starting point is a small development workspace with five distinct parts: ",{"type":24,"tag":353,"props":1497,"children":1500},{"href":1498,"rel":1499},"https://opencode.ai/docs/",[357],[1501],{"type":29,"value":1502},"OpenCode",{"type":29,"value":1504}," to work in the repository, ",{"type":24,"tag":14,"props":1506,"children":1508},{"className":1507},[],[1509],{"type":29,"value":1510},"AGENTS.md",{"type":29,"value":1512}," to explain the repository, ",{"type":24,"tag":353,"props":1514,"children":1517},{"href":1515,"rel":1516},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/getting-started.md",[357],[1518],{"type":29,"value":1519},"OpenSpec",{"type":29,"value":1521}," to make a change reviewable, and custom agents and skills for work that benefits from a repeatable role or procedure. You do not need to configure every part on day one. It helps to understand what question each one answers.",{"type":24,"tag":62,"props":1523,"children":1524},{},[1525,1546],{"type":24,"tag":66,"props":1526,"children":1527},{},[1528],{"type":24,"tag":70,"props":1529,"children":1530},{},[1531,1536,1541],{"type":24,"tag":74,"props":1532,"children":1533},{},[1534],{"type":29,"value":1535},"Piece",{"type":24,"tag":74,"props":1537,"children":1538},{},[1539],{"type":29,"value":1540},"Question it answers",{"type":24,"tag":74,"props":1542,"children":1543},{},[1544],{"type":29,"value":1545},"Example",{"type":24,"tag":85,"props":1547,"children":1548},{},[1549,1566,1587,1604,1622],{"type":24,"tag":70,"props":1550,"children":1551},{},[1552,1556,1561],{"type":24,"tag":92,"props":1553,"children":1554},{},[1555],{"type":29,"value":1502},{"type":24,"tag":92,"props":1557,"children":1558},{},[1559],{"type":29,"value":1560},"Who can inspect and change this repository with me?",{"type":24,"tag":92,"props":1562,"children":1563},{},[1564],{"type":29,"value":1565},"Explore the API, edit code, run tests",{"type":24,"tag":70,"props":1567,"children":1568},{},[1569,1577,1582],{"type":24,"tag":92,"props":1570,"children":1571},{},[1572],{"type":24,"tag":14,"props":1573,"children":1575},{"className":1574},[],[1576],{"type":29,"value":1510},{"type":24,"tag":92,"props":1578,"children":1579},{},[1580],{"type":29,"value":1581},"What should any agent know about this project?",{"type":24,"tag":92,"props":1583,"children":1584},{},[1585],{"type":29,"value":1586},"Architecture, commands, conventions",{"type":24,"tag":70,"props":1588,"children":1589},{},[1590,1594,1599],{"type":24,"tag":92,"props":1591,"children":1592},{},[1593],{"type":29,"value":1519},{"type":24,"tag":92,"props":1595,"children":1596},{},[1597],{"type":29,"value":1598},"What behavior are we agreeing to change?",{"type":24,"tag":92,"props":1600,"children":1601},{},[1602],{"type":29,"value":1603},"A named change with scenarios and tasks",{"type":24,"tag":70,"props":1605,"children":1606},{},[1607,1612,1617],{"type":24,"tag":92,"props":1608,"children":1609},{},[1610],{"type":29,"value":1611},"Custom agent",{"type":24,"tag":92,"props":1613,"children":1614},{},[1615],{"type":29,"value":1616},"Who should take a specialized role?",{"type":24,"tag":92,"props":1618,"children":1619},{},[1620],{"type":29,"value":1621},"A reviewer with no edit permission",{"type":24,"tag":70,"props":1623,"children":1624},{},[1625,1630,1635],{"type":24,"tag":92,"props":1626,"children":1627},{},[1628],{"type":29,"value":1629},"Skill",{"type":24,"tag":92,"props":1631,"children":1632},{},[1633],{"type":29,"value":1634},"How do we repeat a particular procedure?",{"type":24,"tag":92,"props":1636,"children":1637},{},[1638],{"type":29,"value":1639},"A checklist for reviewing API compatibility",{"type":24,"tag":25,"props":1641,"children":1642},{},[1643,1645,1651,1653,1659],{"type":29,"value":1644},"These are different layers, not competing ways to write a longer prompt. Let's assemble them around a small example: an existing task API needs an optional ",{"type":24,"tag":14,"props":1646,"children":1648},{"className":1647},[],[1649],{"type":29,"value":1650},"status",{"type":29,"value":1652}," filter on ",{"type":24,"tag":14,"props":1654,"children":1656},{"className":1655},[],[1657],{"type":29,"value":1658},"GET /tasks",{"type":29,"value":1422},{"type":24,"tag":50,"props":1661,"children":1663},{"id":1662},"start-with-repository-context",[1664],{"type":29,"value":1665},"Start with repository context",{"type":24,"tag":25,"props":1667,"children":1668},{},[1669,1671,1677],{"type":29,"value":1670},"Install OpenCode using one of the methods in its ",{"type":24,"tag":353,"props":1672,"children":1674},{"href":1498,"rel":1673},[357],[1675],{"type":29,"value":1676},"official setup guide",{"type":29,"value":1678},". For a Node-based setup, the documented command is:",{"type":24,"tag":206,"props":1680,"children":1682},{"className":208,"code":1681,"language":210,"meta":7,"style":7},"npm install -g opencode-ai\ncd path/to/your-project\nopencode\n",[1683],{"type":24,"tag":14,"props":1684,"children":1685},{"__ignoreMap":7},[1686,1709,1722],{"type":24,"tag":216,"props":1687,"children":1688},{"class":218,"line":219},[1689,1694,1699,1704],{"type":24,"tag":216,"props":1690,"children":1691},{"style":223},[1692],{"type":29,"value":1693},"npm",{"type":24,"tag":216,"props":1695,"children":1696},{"style":229},[1697],{"type":29,"value":1698}," install",{"type":24,"tag":216,"props":1700,"children":1701},{"style":245},[1702],{"type":29,"value":1703}," -g",{"type":24,"tag":216,"props":1705,"children":1706},{"style":229},[1707],{"type":29,"value":1708}," opencode-ai\n",{"type":24,"tag":216,"props":1710,"children":1711},{"class":218,"line":251},[1712,1717],{"type":24,"tag":216,"props":1713,"children":1714},{"style":245},[1715],{"type":29,"value":1716},"cd",{"type":24,"tag":216,"props":1718,"children":1719},{"style":229},[1720],{"type":29,"value":1721}," path/to/your-project\n",{"type":24,"tag":216,"props":1723,"children":1724},{"class":218,"line":265},[1725],{"type":24,"tag":216,"props":1726,"children":1727},{"style":223},[1728],{"type":29,"value":1729},"opencode\n",{"type":24,"tag":25,"props":1731,"children":1732},{},[1733,1735,1740],{"type":29,"value":1734},"Connect a model provider using OpenCode's setup flow. Before asking it to implement the filter, let it inspect the project: “Where is ",{"type":24,"tag":14,"props":1736,"children":1738},{"className":1737},[],[1739],{"type":29,"value":1658},{"type":29,"value":1741}," implemented, and which tests define its current behavior?” A useful answer should point to actual files and acknowledge uncertainty. If it invents an endpoint, stop there; adding more roles will not repair missing context.",{"type":24,"tag":25,"props":1743,"children":1744},{},[1745,1747,1752,1754,1760,1762,1769],{"type":29,"value":1746},"OpenCode can create a starter ",{"type":24,"tag":14,"props":1748,"children":1750},{"className":1749},[],[1751],{"type":29,"value":1510},{"type":29,"value":1753}," with ",{"type":24,"tag":14,"props":1755,"children":1757},{"className":1756},[],[1758],{"type":29,"value":1759},"/init",{"type":29,"value":1761},". Review what it writes. This file is durable project context, so it should contain facts and working rules that future sessions need, rather than a transcript of today's task. OpenCode's ",{"type":24,"tag":353,"props":1763,"children":1766},{"href":1764,"rel":1765},"https://opencode.ai/docs/rules/",[357],[1767],{"type":29,"value":1768},"rules documentation",{"type":29,"value":1770}," recommends project-specific instructions such as build commands, architecture, and conventions. A compact example:",{"type":24,"tag":206,"props":1772,"children":1774},{"className":370,"code":1773,"language":372,"meta":7,"style":7},"# Task API\n\n- Routes live in `src/http`; persistence lives in `src/repositories`.\n- Preserve the response shape of existing endpoints.\n- Run `npm test` for behavior changes and `npm run build` before handoff.\n- Do not modify database migrations that have already been applied.\n- Record API contract changes in `openapi.yaml`.\n",[1775],{"type":24,"tag":14,"props":1776,"children":1777},{"__ignoreMap":7},[1778,1786,1793,1801,1809,1817,1825],{"type":24,"tag":216,"props":1779,"children":1780},{"class":218,"line":219},[1781],{"type":24,"tag":216,"props":1782,"children":1783},{},[1784],{"type":29,"value":1785},"# Task API\n",{"type":24,"tag":216,"props":1787,"children":1788},{"class":218,"line":251},[1789],{"type":24,"tag":216,"props":1790,"children":1791},{"emptyLinePlaceholder":390},[1792],{"type":29,"value":393},{"type":24,"tag":216,"props":1794,"children":1795},{"class":218,"line":265},[1796],{"type":24,"tag":216,"props":1797,"children":1798},{},[1799],{"type":29,"value":1800},"- Routes live in `src/http`; persistence lives in `src/repositories`.\n",{"type":24,"tag":216,"props":1802,"children":1803},{"class":218,"line":404},[1804],{"type":24,"tag":216,"props":1805,"children":1806},{},[1807],{"type":29,"value":1808},"- Preserve the response shape of existing endpoints.\n",{"type":24,"tag":216,"props":1810,"children":1811},{"class":218,"line":413},[1812],{"type":24,"tag":216,"props":1813,"children":1814},{},[1815],{"type":29,"value":1816},"- Run `npm test` for behavior changes and `npm run build` before handoff.\n",{"type":24,"tag":216,"props":1818,"children":1819},{"class":218,"line":421},[1820],{"type":24,"tag":216,"props":1821,"children":1822},{},[1823],{"type":29,"value":1824},"- Do not modify database migrations that have already been applied.\n",{"type":24,"tag":216,"props":1826,"children":1827},{"class":218,"line":430},[1828],{"type":24,"tag":216,"props":1829,"children":1830},{},[1831],{"type":29,"value":1832},"- Record API contract changes in `openapi.yaml`.\n",{"type":24,"tag":25,"props":1834,"children":1835},{},[1836,1838,1844],{"type":29,"value":1837},"The exact paths and commands must match your repository. If your actual check is ",{"type":24,"tag":14,"props":1839,"children":1841},{"className":1840},[],[1842],{"type":29,"value":1843},"./gradlew test",{"type":29,"value":1845},", write that instead. Treat this file like code: review it, keep it short, and update it when the project changes. It should help an agent avoid avoidable mistakes; it cannot guarantee correctness.",{"type":24,"tag":50,"props":1847,"children":1849},{"id":1848},"give-the-change-a-contract-with-openspec",[1850],{"type":29,"value":1851},"Give the change a contract with OpenSpec",{"type":24,"tag":25,"props":1853,"children":1854},{},[1855],{"type":29,"value":1856},"The request “add a status filter” still leaves questions. Which statuses are valid? What happens when the parameter is omitted? Is an unknown status an empty result or a client error? Does filtering happen before pagination? Those decisions affect clients, so I want them visible before implementation.",{"type":24,"tag":25,"props":1858,"children":1859},{},[1860,1862,1869],{"type":29,"value":1861},"OpenSpec can be installed and initialized from the terminal. Its ",{"type":24,"tag":353,"props":1863,"children":1866},{"href":1864,"rel":1865},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md",[357],[1867],{"type":29,"value":1868},"installation guide",{"type":29,"value":1870}," documents the Node requirement and setup commands:",{"type":24,"tag":206,"props":1872,"children":1874},{"className":208,"code":1873,"language":210,"meta":7,"style":7},"npm install -g @fission-ai/openspec@latest\nopenspec init --tools opencode\n",[1875],{"type":24,"tag":14,"props":1876,"children":1877},{"__ignoreMap":7},[1878,1898],{"type":24,"tag":216,"props":1879,"children":1880},{"class":218,"line":219},[1881,1885,1889,1893],{"type":24,"tag":216,"props":1882,"children":1883},{"style":223},[1884],{"type":29,"value":1693},{"type":24,"tag":216,"props":1886,"children":1887},{"style":229},[1888],{"type":29,"value":1698},{"type":24,"tag":216,"props":1890,"children":1891},{"style":245},[1892],{"type":29,"value":1703},{"type":24,"tag":216,"props":1894,"children":1895},{"style":229},[1896],{"type":29,"value":1897}," @fission-ai/openspec@latest\n",{"type":24,"tag":216,"props":1899,"children":1900},{"class":218,"line":251},[1901,1905,1910,1915],{"type":24,"tag":216,"props":1902,"children":1903},{"style":223},[1904],{"type":29,"value":226},{"type":24,"tag":216,"props":1906,"children":1907},{"style":229},[1908],{"type":29,"value":1909}," init",{"type":24,"tag":216,"props":1911,"children":1912},{"style":245},[1913],{"type":29,"value":1914}," --tools",{"type":24,"tag":216,"props":1916,"children":1917},{"style":229},[1918],{"type":29,"value":1919}," opencode\n",{"type":24,"tag":25,"props":1921,"children":1922},{},[1923,1925,1931,1933,1939,1941,1948],{"type":29,"value":1924},"Run ",{"type":24,"tag":14,"props":1926,"children":1928},{"className":1927},[],[1929],{"type":29,"value":1930},"openspec init",{"type":29,"value":1932}," at the project root. The ",{"type":24,"tag":14,"props":1934,"children":1936},{"className":1935},[],[1937],{"type":29,"value":1938},"--tools opencode",{"type":29,"value":1940}," option selects the OpenCode integration without an interactive picker. OpenSpec will create its planning directory and the workflow files for that integration. Check the generated files rather than assuming that every tool exposes the same slash-command names; OpenSpec's ",{"type":24,"tag":353,"props":1942,"children":1945},{"href":1943,"rel":1944},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/supported-tools.md",[357],[1946],{"type":29,"value":1947},"tool reference",{"type":29,"value":1949}," describes those differences.",{"type":24,"tag":25,"props":1951,"children":1952},{},[1953,1955,1961],{"type":29,"value":1954},"In OpenCode, the OpenSpec integration currently provides ",{"type":24,"tag":14,"props":1956,"children":1958},{"className":1957},[],[1959],{"type":29,"value":1960},"/opsx-propose",{"type":29,"value":1962}," for this step. Start a named change in the chat:",{"type":24,"tag":206,"props":1964,"children":1966},{"className":1030,"code":1965,"language":29,"meta":7,"style":7},"/opsx-propose add-task-status-filter\n",[1967],{"type":24,"tag":14,"props":1968,"children":1969},{"__ignoreMap":7},[1970],{"type":24,"tag":216,"props":1971,"children":1972},{"class":218,"line":219},[1973],{"type":24,"tag":216,"props":1974,"children":1975},{},[1976],{"type":29,"value":1965},{"type":24,"tag":25,"props":1978,"children":1979},{},[1980],{"type":29,"value":1981},"Give it the context the command needs:",{"type":24,"tag":206,"props":1983,"children":1985},{"className":1030,"code":1984,"language":29,"meta":7,"style":7},"Propose a change to add an optional status filter to GET /tasks.\nInspect the current endpoint and tests first. Define the behavior when the\nparameter is absent, when it is valid, and when it is invalid. Do not\nimplement the change until we have reviewed the proposal and scenarios.\n",[1986],{"type":24,"tag":14,"props":1987,"children":1988},{"__ignoreMap":7},[1989,1997,2005,2013],{"type":24,"tag":216,"props":1990,"children":1991},{"class":218,"line":219},[1992],{"type":24,"tag":216,"props":1993,"children":1994},{},[1995],{"type":29,"value":1996},"Propose a change to add an optional status filter to GET /tasks.\n",{"type":24,"tag":216,"props":1998,"children":1999},{"class":218,"line":251},[2000],{"type":24,"tag":216,"props":2001,"children":2002},{},[2003],{"type":29,"value":2004},"Inspect the current endpoint and tests first. Define the behavior when the\n",{"type":24,"tag":216,"props":2006,"children":2007},{"class":218,"line":265},[2008],{"type":24,"tag":216,"props":2009,"children":2010},{},[2011],{"type":29,"value":2012},"parameter is absent, when it is valid, and when it is invalid. Do not\n",{"type":24,"tag":216,"props":2014,"children":2015},{"class":218,"line":404},[2016],{"type":24,"tag":216,"props":2017,"children":2018},{},[2019],{"type":29,"value":2020},"implement the change until we have reviewed the proposal and scenarios.\n",{"type":24,"tag":25,"props":2022,"children":2023},{},[2024,2026,2031,2033,2037,2039,2044],{"type":29,"value":2025},"The generated command is part of the OpenSpec workflow installed for OpenCode; check the names printed by ",{"type":24,"tag":14,"props":2027,"children":2029},{"className":2028},[],[2030],{"type":29,"value":1930},{"type":29,"value":2032}," if your installation differs. In the default spec-driven structure, a proposal explains ",{"type":24,"tag":1127,"props":2034,"children":2035},{},[2036],{"type":29,"value":319},{"type":29,"value":2038},", a spec describes ",{"type":24,"tag":1127,"props":2040,"children":2041},{},[2042],{"type":29,"value":2043},"observable behavior",{"type":29,"value":2045},", a design captures technical decisions when needed, and tasks track implementation. A scenario might say:",{"type":24,"tag":206,"props":2047,"children":2049},{"className":370,"code":2048,"language":372,"meta":7,"style":7},"### Requirement: Filter tasks by status\nThe API MUST allow clients to filter tasks by a supported status.\n\n#### Scenario: No filter\n- **WHEN** a client calls GET /tasks without a status parameter\n- **THEN** the existing unfiltered behavior is preserved\n\n#### Scenario: Unsupported status\n- **WHEN** a client requests a status the API does not support\n- **THEN** the API returns the documented validation error\n",[2050],{"type":24,"tag":14,"props":2051,"children":2052},{"__ignoreMap":7},[2053,2061,2069,2076,2084,2092,2100,2107,2115,2123],{"type":24,"tag":216,"props":2054,"children":2055},{"class":218,"line":219},[2056],{"type":24,"tag":216,"props":2057,"children":2058},{},[2059],{"type":29,"value":2060},"### Requirement: Filter tasks by status\n",{"type":24,"tag":216,"props":2062,"children":2063},{"class":218,"line":251},[2064],{"type":24,"tag":216,"props":2065,"children":2066},{},[2067],{"type":29,"value":2068},"The API MUST allow clients to filter tasks by a supported status.\n",{"type":24,"tag":216,"props":2070,"children":2071},{"class":218,"line":265},[2072],{"type":24,"tag":216,"props":2073,"children":2074},{"emptyLinePlaceholder":390},[2075],{"type":29,"value":393},{"type":24,"tag":216,"props":2077,"children":2078},{"class":218,"line":404},[2079],{"type":24,"tag":216,"props":2080,"children":2081},{},[2082],{"type":29,"value":2083},"#### Scenario: No filter\n",{"type":24,"tag":216,"props":2085,"children":2086},{"class":218,"line":413},[2087],{"type":24,"tag":216,"props":2088,"children":2089},{},[2090],{"type":29,"value":2091},"- **WHEN** a client calls GET /tasks without a status parameter\n",{"type":24,"tag":216,"props":2093,"children":2094},{"class":218,"line":421},[2095],{"type":24,"tag":216,"props":2096,"children":2097},{},[2098],{"type":29,"value":2099},"- **THEN** the existing unfiltered behavior is preserved\n",{"type":24,"tag":216,"props":2101,"children":2102},{"class":218,"line":430},[2103],{"type":24,"tag":216,"props":2104,"children":2105},{"emptyLinePlaceholder":390},[2106],{"type":29,"value":393},{"type":24,"tag":216,"props":2108,"children":2109},{"class":218,"line":439},[2110],{"type":24,"tag":216,"props":2111,"children":2112},{},[2113],{"type":29,"value":2114},"#### Scenario: Unsupported status\n",{"type":24,"tag":216,"props":2116,"children":2117},{"class":218,"line":448},[2118],{"type":24,"tag":216,"props":2119,"children":2120},{},[2121],{"type":29,"value":2122},"- **WHEN** a client requests a status the API does not support\n",{"type":24,"tag":216,"props":2124,"children":2125},{"class":218,"line":457},[2126],{"type":24,"tag":216,"props":2127,"children":2128},{},[2129],{"type":29,"value":2130},"- **THEN** the API returns the documented validation error\n",{"type":24,"tag":25,"props":2132,"children":2133},{},[2134,2136,2142],{"type":29,"value":2135},"That is only an example contract. Your API may deliberately handle invalid values differently. Decide it with the people who own the API, then write the chosen behavior down. For a deeper walkthrough of proposals, scenarios, and design trade-offs, see my ",{"type":24,"tag":353,"props":2137,"children":2139},{"href":2138},"/spec-driven-development-openspec/",[2140],{"type":29,"value":2141},"OpenSpec article",{"type":29,"value":1422},{"type":24,"tag":50,"props":2144,"children":2146},{"id":2145},"add-a-specialist-only-when-it-has-a-clear-job",[2147],{"type":29,"value":2148},"Add a specialist only when it has a clear job",{"type":24,"tag":25,"props":2150,"children":2151},{},[2152,2154,2161],{"type":29,"value":2153},"Once the change is implemented, a second perspective can help. OpenCode supports ",{"type":24,"tag":353,"props":2155,"children":2158},{"href":2156,"rel":2157},"https://opencode.ai/docs/agents/",[357],[2159],{"type":29,"value":2160},"custom primary agents and subagents",{"type":29,"value":2162},". A custom agent has a role and its own permissions; it is not a second specification. For this example, a review subagent should read the diff and contract, question edge cases, and report findings without editing files.",{"type":24,"tag":25,"props":2164,"children":2165},{},[2166,2168,2174],{"type":29,"value":2167},"Create ",{"type":24,"tag":14,"props":2169,"children":2171},{"className":2170},[],[2172],{"type":29,"value":2173},".opencode/agents/api-reviewer.md",{"type":29,"value":2175},":",{"type":24,"tag":206,"props":2177,"children":2179},{"className":370,"code":2178,"language":372,"meta":7,"style":7},"---\ndescription: Reviews API changes against their contract and existing clients\nmode: subagent\npermission:\n  edit: deny\n  bash: ask\n---\n\nReview the proposed API change and its tests. Compare the implementation\nwith the OpenSpec scenarios and the existing endpoint behavior. Look for\ncompatibility changes, pagination mistakes, invalid input handling, and\nmissing tests. Cite the files and lines behind each finding. Do not edit.\n",[2180],{"type":24,"tag":14,"props":2181,"children":2182},{"__ignoreMap":7},[2183,2191,2199,2207,2215,2223,2231,2238,2245,2253,2261,2269],{"type":24,"tag":216,"props":2184,"children":2185},{"class":218,"line":219},[2186],{"type":24,"tag":216,"props":2187,"children":2188},{},[2189],{"type":29,"value":2190},"---\n",{"type":24,"tag":216,"props":2192,"children":2193},{"class":218,"line":251},[2194],{"type":24,"tag":216,"props":2195,"children":2196},{},[2197],{"type":29,"value":2198},"description: Reviews API changes against their contract and existing clients\n",{"type":24,"tag":216,"props":2200,"children":2201},{"class":218,"line":265},[2202],{"type":24,"tag":216,"props":2203,"children":2204},{},[2205],{"type":29,"value":2206},"mode: subagent\n",{"type":24,"tag":216,"props":2208,"children":2209},{"class":218,"line":404},[2210],{"type":24,"tag":216,"props":2211,"children":2212},{},[2213],{"type":29,"value":2214},"permission:\n",{"type":24,"tag":216,"props":2216,"children":2217},{"class":218,"line":413},[2218],{"type":24,"tag":216,"props":2219,"children":2220},{},[2221],{"type":29,"value":2222},"  edit: deny\n",{"type":24,"tag":216,"props":2224,"children":2225},{"class":218,"line":421},[2226],{"type":24,"tag":216,"props":2227,"children":2228},{},[2229],{"type":29,"value":2230},"  bash: ask\n",{"type":24,"tag":216,"props":2232,"children":2233},{"class":218,"line":430},[2234],{"type":24,"tag":216,"props":2235,"children":2236},{},[2237],{"type":29,"value":2190},{"type":24,"tag":216,"props":2239,"children":2240},{"class":218,"line":439},[2241],{"type":24,"tag":216,"props":2242,"children":2243},{"emptyLinePlaceholder":390},[2244],{"type":29,"value":393},{"type":24,"tag":216,"props":2246,"children":2247},{"class":218,"line":448},[2248],{"type":24,"tag":216,"props":2249,"children":2250},{},[2251],{"type":29,"value":2252},"Review the proposed API change and its tests. Compare the implementation\n",{"type":24,"tag":216,"props":2254,"children":2255},{"class":218,"line":457},[2256],{"type":24,"tag":216,"props":2257,"children":2258},{},[2259],{"type":29,"value":2260},"with the OpenSpec scenarios and the existing endpoint behavior. Look for\n",{"type":24,"tag":216,"props":2262,"children":2263},{"class":218,"line":465},[2264],{"type":24,"tag":216,"props":2265,"children":2266},{},[2267],{"type":29,"value":2268},"compatibility changes, pagination mistakes, invalid input handling, and\n",{"type":24,"tag":216,"props":2270,"children":2271},{"class":218,"line":474},[2272],{"type":24,"tag":216,"props":2273,"children":2274},{},[2275],{"type":29,"value":2276},"missing tests. Cite the files and lines behind each finding. Do not edit.\n",{"type":24,"tag":25,"props":2278,"children":2279},{},[2280,2282,2288,2290,2296,2298,2305],{"type":29,"value":2281},"You can invoke that subagent by mentioning ",{"type":24,"tag":14,"props":2283,"children":2285},{"className":2284},[],[2286],{"type":29,"value":2287},"@api-reviewer",{"type":29,"value":2289}," in OpenCode. It cannot edit files, and it must ask before running a shell command such as ",{"type":24,"tag":14,"props":2291,"children":2293},{"className":2292},[],[2294],{"type":29,"value":2295},"git diff",{"type":29,"value":2297},". Those permissions make its role explicit. They do not make its analysis infallible: the engineer still decides whether each finding is correct and whether the implementation satisfies the contract. OpenCode's ",{"type":24,"tag":353,"props":2299,"children":2302},{"href":2300,"rel":2301},"https://opencode.ai/docs/permissions/",[357],[2303],{"type":29,"value":2304},"permissions guide",{"type":29,"value":2306}," is worth reading before giving a specialist shell or editing access.",{"type":24,"tag":25,"props":2308,"children":2309},{},[2310],{"type":29,"value":2311},"Do you need separate “backend”, “tester”, “architect”, and “reviewer” agents for this endpoint? Probably not. Start with the general agent and add a specialist when you can name a recurring job, its inputs, its output, and the permissions it needs. More agents can also create more handoffs and inconsistent assumptions.",{"type":24,"tag":50,"props":2313,"children":2315},{"id":2314},"capture-a-procedure-as-a-skill",[2316],{"type":29,"value":2317},"Capture a procedure as a skill",{"type":24,"tag":25,"props":2319,"children":2320},{},[2321,2323,2327,2329,2335],{"type":29,"value":2322},"A skill is different from an agent. It packages instructions for ",{"type":24,"tag":1127,"props":2324,"children":2325},{},[2326],{"type":29,"value":341},{"type":29,"value":2328}," to do a recurring task, and OpenCode loads it when relevant. For instance, the API reviewer's role is “review this change”; an ",{"type":24,"tag":14,"props":2330,"children":2332},{"className":2331},[],[2333],{"type":29,"value":2334},"api-compatibility",{"type":29,"value":2336}," skill could define the checks the reviewer should apply across many API changes.",{"type":24,"tag":25,"props":2338,"children":2339},{},[2340,2341,2347],{"type":29,"value":2167},{"type":24,"tag":14,"props":2342,"children":2344},{"className":2343},[],[2345],{"type":29,"value":2346},".opencode/skills/api-compatibility/SKILL.md",{"type":29,"value":2175},{"type":24,"tag":206,"props":2349,"children":2351},{"className":370,"code":2350,"language":372,"meta":7,"style":7},"---\nname: api-compatibility\ndescription: Check an API change for client-visible compatibility risks\n---\n\n## Use this skill when\n\nAn endpoint, request, response, or documented error behavior changes.\n\n## Procedure\n\n1. Read the current API contract and at least one existing client call.\n2. Compare old and new request and response shapes.\n3. Check defaults, invalid input, pagination, and error status codes.\n4. Identify changes that require clients to deploy at the same time.\n5. Report each risk with evidence and a compatible migration option.\n",[2352],{"type":24,"tag":14,"props":2353,"children":2354},{"__ignoreMap":7},[2355,2362,2370,2378,2385,2392,2400,2407,2415,2422,2430,2437,2445,2453,2461,2469],{"type":24,"tag":216,"props":2356,"children":2357},{"class":218,"line":219},[2358],{"type":24,"tag":216,"props":2359,"children":2360},{},[2361],{"type":29,"value":2190},{"type":24,"tag":216,"props":2363,"children":2364},{"class":218,"line":251},[2365],{"type":24,"tag":216,"props":2366,"children":2367},{},[2368],{"type":29,"value":2369},"name: api-compatibility\n",{"type":24,"tag":216,"props":2371,"children":2372},{"class":218,"line":265},[2373],{"type":24,"tag":216,"props":2374,"children":2375},{},[2376],{"type":29,"value":2377},"description: Check an API change for client-visible compatibility risks\n",{"type":24,"tag":216,"props":2379,"children":2380},{"class":218,"line":404},[2381],{"type":24,"tag":216,"props":2382,"children":2383},{},[2384],{"type":29,"value":2190},{"type":24,"tag":216,"props":2386,"children":2387},{"class":218,"line":413},[2388],{"type":24,"tag":216,"props":2389,"children":2390},{"emptyLinePlaceholder":390},[2391],{"type":29,"value":393},{"type":24,"tag":216,"props":2393,"children":2394},{"class":218,"line":421},[2395],{"type":24,"tag":216,"props":2396,"children":2397},{},[2398],{"type":29,"value":2399},"## Use this skill when\n",{"type":24,"tag":216,"props":2401,"children":2402},{"class":218,"line":430},[2403],{"type":24,"tag":216,"props":2404,"children":2405},{"emptyLinePlaceholder":390},[2406],{"type":29,"value":393},{"type":24,"tag":216,"props":2408,"children":2409},{"class":218,"line":439},[2410],{"type":24,"tag":216,"props":2411,"children":2412},{},[2413],{"type":29,"value":2414},"An endpoint, request, response, or documented error behavior changes.\n",{"type":24,"tag":216,"props":2416,"children":2417},{"class":218,"line":448},[2418],{"type":24,"tag":216,"props":2419,"children":2420},{"emptyLinePlaceholder":390},[2421],{"type":29,"value":393},{"type":24,"tag":216,"props":2423,"children":2424},{"class":218,"line":457},[2425],{"type":24,"tag":216,"props":2426,"children":2427},{},[2428],{"type":29,"value":2429},"## Procedure\n",{"type":24,"tag":216,"props":2431,"children":2432},{"class":218,"line":465},[2433],{"type":24,"tag":216,"props":2434,"children":2435},{"emptyLinePlaceholder":390},[2436],{"type":29,"value":393},{"type":24,"tag":216,"props":2438,"children":2439},{"class":218,"line":474},[2440],{"type":24,"tag":216,"props":2441,"children":2442},{},[2443],{"type":29,"value":2444},"1. Read the current API contract and at least one existing client call.\n",{"type":24,"tag":216,"props":2446,"children":2447},{"class":218,"line":483},[2448],{"type":24,"tag":216,"props":2449,"children":2450},{},[2451],{"type":29,"value":2452},"2. Compare old and new request and response shapes.\n",{"type":24,"tag":216,"props":2454,"children":2455},{"class":218,"line":492},[2456],{"type":24,"tag":216,"props":2457,"children":2458},{},[2459],{"type":29,"value":2460},"3. Check defaults, invalid input, pagination, and error status codes.\n",{"type":24,"tag":216,"props":2462,"children":2463},{"class":218,"line":500},[2464],{"type":24,"tag":216,"props":2465,"children":2466},{},[2467],{"type":29,"value":2468},"4. Identify changes that require clients to deploy at the same time.\n",{"type":24,"tag":216,"props":2470,"children":2471},{"class":218,"line":509},[2472],{"type":24,"tag":216,"props":2473,"children":2474},{},[2475],{"type":29,"value":2476},"5. Report each risk with evidence and a compatible migration option.\n",{"type":24,"tag":25,"props":2478,"children":2479},{},[2480,2482,2489,2491,2497,2499,2505],{"type":29,"value":2481},"OpenCode's ",{"type":24,"tag":353,"props":2483,"children":2486},{"href":2484,"rel":2485},"https://opencode.ai/docs/skills/",[357],[2487],{"type":29,"value":2488},"skill documentation",{"type":29,"value":2490}," describes this directory structure and the required ",{"type":24,"tag":14,"props":2492,"children":2494},{"className":2493},[],[2495],{"type":29,"value":2496},"name",{"type":29,"value":2498}," and ",{"type":24,"tag":14,"props":2500,"children":2502},{"className":2501},[],[2503],{"type":29,"value":2504},"description",{"type":29,"value":2506}," frontmatter. Keep the description specific: it helps the agent decide whether to load the skill. A skill should carry a procedure that survives one task. If it merely repeats today's prompt, it probably belongs in the conversation instead.",{"type":24,"tag":25,"props":2508,"children":2509},{},[2510,2512,2517],{"type":29,"value":2511},"You can ask the reviewer to use ",{"type":24,"tag":14,"props":2513,"children":2515},{"className":2514},[],[2516],{"type":29,"value":2334},{"type":29,"value":2518},", or let the agent discover it when the task matches its description. For a small project, you might not need this file yet. Write it when you notice yourself repeating the same review steps across changes.",{"type":24,"tag":50,"props":2520,"children":2522},{"id":2521},"run-one-complete-change",[2523],{"type":29,"value":2524},"Run one complete change",{"type":24,"tag":25,"props":2526,"children":2527},{},[2528],{"type":29,"value":2529},"With those pieces in place, the loop is straightforward:",{"type":24,"tag":2531,"props":2532,"children":2533},"ol",{},[2534,2539,2544,2563,2575],{"type":24,"tag":1412,"props":2535,"children":2536},{},[2537],{"type":29,"value":2538},"Ask OpenCode to inspect the current endpoint and tests.",{"type":24,"tag":1412,"props":2540,"children":2541},{},[2542],{"type":29,"value":2543},"Use OpenSpec to propose the filter and review its scenarios. Resolve ambiguous client behavior before coding.",{"type":24,"tag":1412,"props":2545,"children":2546},{},[2547,2549,2555,2557,2562],{"type":29,"value":2548},"Use ",{"type":24,"tag":14,"props":2550,"children":2552},{"className":2551},[],[2553],{"type":29,"value":2554},"/opsx-apply",{"type":29,"value":2556}," to implement the approved change, then run the repository checks from ",{"type":24,"tag":14,"props":2558,"children":2560},{"className":2559},[],[2561],{"type":29,"value":1510},{"type":29,"value":1422},{"type":24,"tag":1412,"props":2564,"children":2565},{},[2566,2568,2573],{"type":29,"value":2567},"Ask ",{"type":24,"tag":14,"props":2569,"children":2571},{"className":2570},[],[2572],{"type":29,"value":2287},{"type":29,"value":2574}," to compare the result with the spec and apply the compatibility procedure.",{"type":24,"tag":1412,"props":2576,"children":2577},{},[2578,2580,2586],{"type":29,"value":2579},"Read the diff, run the checks yourself or inspect their output, and correct any mismatch. Update the spec if a decision legitimately changed during implementation. Archive the change with ",{"type":24,"tag":14,"props":2581,"children":2583},{"className":2582},[],[2584],{"type":29,"value":2585},"/opsx-archive",{"type":29,"value":2587}," once its implementation is finished and accepted.",{"type":24,"tag":25,"props":2589,"children":2590},{},[2591],{"type":29,"value":2592},"Notice what remains the developer's responsibility: choosing the behavior, assessing trade-offs, checking evidence, and deciding when the change is ready. The workspace helps the agent work with better context and leaves a clearer trail for a human reviewer. It does not turn a generated patch into a trustworthy one by itself.",{"type":24,"tag":25,"props":2594,"children":2595},{},[2596],{"type":29,"value":2597},"There is also no prize for using every component in every repository. Start with OpenCode and accurate project instructions. Add OpenSpec when the risk is implementing the wrong behavior; add a custom agent when a specialized perspective repeatedly helps; add a skill when a procedure deserves to be reused. That progression keeps the setup understandable as the project grows.",{"type":24,"tag":1445,"props":2599,"children":2600},{},[2601],{"type":29,"value":1449},{"title":7,"searchDepth":251,"depth":251,"links":2603},[2604,2605,2606,2607,2608],{"id":1662,"depth":251,"text":1665},{"id":1848,"depth":251,"text":1851},{"id":2145,"depth":251,"text":2148},{"id":2314,"depth":251,"text":2317},{"id":2521,"depth":251,"text":2524},"content:posts:ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec",{"_path":2613,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2614,"description":2615,"layout":10,"date":2616,"subtitle":2617,"image":2618,"optimized_image":2618,"category":14,"tags":2619,"author":19,"paginate":6,"body":2621,"_type":372,"_id":3797,"_source":1463,"_file":3798,"_stem":3799,"_extension":1466},"/posts/git-worktrees-parallel-development","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.","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",[14,2620,17,1476],"git",{"type":21,"children":2622,"toc":3787},[2623,2628,2640,2645,2651,2696,2751,2772,2777,2783,2802,2913,2963,2984,2989,3038,3058,3064,3069,3089,3217,3222,3235,3241,3246,3285,3290,3295,3300,3306,3331,3409,3434,3439,3502,3535,3561,3567,3572,3614,3665,3700,3706,3711,3739,3744,3748,3783],{"type":24,"tag":25,"props":2624,"children":2625},{},[2626],{"type":29,"value":2627},"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":2629,"children":2630},{},[2631,2633,2638],{"type":29,"value":2632},"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":2634,"children":2635},{},[2636],{"type":29,"value":2637},"Parallel coding only helps when each task has an explicit boundary and the results can be integrated safely.",{"type":29,"value":2639}," Worktrees provide the filesystem boundary; they do not make design decisions or resolve conflicting changes for you.",{"type":24,"tag":25,"props":2641,"children":2642},{},[2643],{"type":29,"value":2644},"Here is a workflow you can try with a repository you already have.",{"type":24,"tag":50,"props":2646,"children":2648},{"id":2647},"what-a-worktree-actually-separates",[2649],{"type":29,"value":2650},"What a worktree actually separates",{"type":24,"tag":25,"props":2652,"children":2653},{},[2654,2656,2662,2664,2670,2672,2678,2680,2686,2688,2695],{"type":29,"value":2655},"A linked worktree has its own checked-out files, ",{"type":24,"tag":14,"props":2657,"children":2659},{"className":2658},[],[2660],{"type":29,"value":2661},"HEAD",{"type":29,"value":2663},", index, and uncommitted changes. The worktrees share the repository's objects and branch references. You can have ",{"type":24,"tag":14,"props":2665,"children":2667},{"className":2666},[],[2668],{"type":29,"value":2669},"main",{"type":29,"value":2671}," open in one directory, ",{"type":24,"tag":14,"props":2673,"children":2675},{"className":2674},[],[2676],{"type":29,"value":2677},"feature/search-index",{"type":29,"value":2679}," in another, and ",{"type":24,"tag":14,"props":2681,"children":2683},{"className":2682},[],[2684],{"type":29,"value":2685},"feature/api-tests",{"type":29,"value":2687}," in a third without cloning the repository three times. Git documents this model in ",{"type":24,"tag":353,"props":2689,"children":2692},{"href":2690,"rel":2691},"https://git-scm.com/docs/git-worktree",[357],[2693],{"type":29,"value":2694},"git-worktree",{"type":29,"value":1422},{"type":24,"tag":206,"props":2697,"children":2699},{"className":1030,"code":2698,"language":29,"meta":7,"style":7},"workspace/\n├── project/                         main; clean integration checkout\n└── .worktrees/\n    └── project/\n        ├── search-index/            feature/search-index\n        └── api-tests/               feature/api-tests\n",[2700],{"type":24,"tag":14,"props":2701,"children":2702},{"__ignoreMap":7},[2703,2711,2719,2727,2735,2743],{"type":24,"tag":216,"props":2704,"children":2705},{"class":218,"line":219},[2706],{"type":24,"tag":216,"props":2707,"children":2708},{},[2709],{"type":29,"value":2710},"workspace/\n",{"type":24,"tag":216,"props":2712,"children":2713},{"class":218,"line":251},[2714],{"type":24,"tag":216,"props":2715,"children":2716},{},[2717],{"type":29,"value":2718},"├── project/                         main; clean integration checkout\n",{"type":24,"tag":216,"props":2720,"children":2721},{"class":218,"line":265},[2722],{"type":24,"tag":216,"props":2723,"children":2724},{},[2725],{"type":29,"value":2726},"└── .worktrees/\n",{"type":24,"tag":216,"props":2728,"children":2729},{"class":218,"line":404},[2730],{"type":24,"tag":216,"props":2731,"children":2732},{},[2733],{"type":29,"value":2734},"    └── project/\n",{"type":24,"tag":216,"props":2736,"children":2737},{"class":218,"line":413},[2738],{"type":24,"tag":216,"props":2739,"children":2740},{},[2741],{"type":29,"value":2742},"        ├── search-index/            feature/search-index\n",{"type":24,"tag":216,"props":2744,"children":2745},{"class":218,"line":421},[2746],{"type":24,"tag":216,"props":2747,"children":2748},{},[2749],{"type":29,"value":2750},"        └── api-tests/               feature/api-tests\n",{"type":24,"tag":25,"props":2752,"children":2753},{},[2754,2756,2762,2764,2770],{"type":29,"value":2755},"This layout keeps task checkouts outside the main checkout, so a file search or build in ",{"type":24,"tag":14,"props":2757,"children":2759},{"className":2758},[],[2760],{"type":29,"value":2761},"project/",{"type":29,"value":2763}," 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":2765,"children":2767},{"className":2766},[],[2768],{"type":29,"value":2769},"git worktree list",{"type":29,"value":2771}," shows every worktree Git knows about, regardless of where you put it.",{"type":24,"tag":25,"props":2773,"children":2774},{},[2775],{"type":29,"value":2776},"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":2778,"children":2780},{"id":2779},"create-two-task-checkouts",[2781],{"type":29,"value":2782},"Create two task checkouts",{"type":24,"tag":25,"props":2784,"children":2785},{},[2786,2788,2793,2795,2800],{"type":29,"value":2787},"Run these commands from the existing ",{"type":24,"tag":14,"props":2789,"children":2791},{"className":2790},[],[2792],{"type":29,"value":2761},{"type":29,"value":2794}," checkout while ",{"type":24,"tag":14,"props":2796,"children":2798},{"className":2797},[],[2799],{"type":29,"value":2669},{"type":29,"value":2801}," is clean:",{"type":24,"tag":206,"props":2803,"children":2805},{"className":208,"code":2804,"language":210,"meta":7,"style":7},"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",[2806],{"type":24,"tag":14,"props":2807,"children":2808},{"__ignoreMap":7},[2809,2827,2864,2897],{"type":24,"tag":216,"props":2810,"children":2811},{"class":218,"line":219},[2812,2817,2822],{"type":24,"tag":216,"props":2813,"children":2814},{"style":223},[2815],{"type":29,"value":2816},"mkdir",{"type":24,"tag":216,"props":2818,"children":2819},{"style":245},[2820],{"type":29,"value":2821}," -p",{"type":24,"tag":216,"props":2823,"children":2824},{"style":229},[2825],{"type":29,"value":2826}," ../.worktrees/project\n",{"type":24,"tag":216,"props":2828,"children":2829},{"class":218,"line":251},[2830,2834,2839,2844,2849,2854,2859],{"type":24,"tag":216,"props":2831,"children":2832},{"style":223},[2833],{"type":29,"value":2620},{"type":24,"tag":216,"props":2835,"children":2836},{"style":229},[2837],{"type":29,"value":2838}," worktree",{"type":24,"tag":216,"props":2840,"children":2841},{"style":229},[2842],{"type":29,"value":2843}," add",{"type":24,"tag":216,"props":2845,"children":2846},{"style":245},[2847],{"type":29,"value":2848}," -b",{"type":24,"tag":216,"props":2850,"children":2851},{"style":229},[2852],{"type":29,"value":2853}," feature/search-index",{"type":24,"tag":216,"props":2855,"children":2856},{"style":229},[2857],{"type":29,"value":2858}," ../.worktrees/project/search-index",{"type":24,"tag":216,"props":2860,"children":2861},{"style":229},[2862],{"type":29,"value":2863}," main\n",{"type":24,"tag":216,"props":2865,"children":2866},{"class":218,"line":265},[2867,2871,2875,2879,2883,2888,2893],{"type":24,"tag":216,"props":2868,"children":2869},{"style":223},[2870],{"type":29,"value":2620},{"type":24,"tag":216,"props":2872,"children":2873},{"style":229},[2874],{"type":29,"value":2838},{"type":24,"tag":216,"props":2876,"children":2877},{"style":229},[2878],{"type":29,"value":2843},{"type":24,"tag":216,"props":2880,"children":2881},{"style":245},[2882],{"type":29,"value":2848},{"type":24,"tag":216,"props":2884,"children":2885},{"style":229},[2886],{"type":29,"value":2887}," feature/api-tests",{"type":24,"tag":216,"props":2889,"children":2890},{"style":229},[2891],{"type":29,"value":2892}," ../.worktrees/project/api-tests",{"type":24,"tag":216,"props":2894,"children":2895},{"style":229},[2896],{"type":29,"value":2863},{"type":24,"tag":216,"props":2898,"children":2899},{"class":218,"line":404},[2900,2904,2908],{"type":24,"tag":216,"props":2901,"children":2902},{"style":223},[2903],{"type":29,"value":2620},{"type":24,"tag":216,"props":2905,"children":2906},{"style":229},[2907],{"type":29,"value":2838},{"type":24,"tag":216,"props":2909,"children":2910},{"style":229},[2911],{"type":29,"value":2912}," list\n",{"type":24,"tag":25,"props":2914,"children":2915},{},[2916,2918,2924,2926,2931,2933,2938,2940,2947,2949,2955,2956,2962],{"type":29,"value":2917},"The ",{"type":24,"tag":14,"props":2919,"children":2921},{"className":2920},[],[2922],{"type":29,"value":2923},"-b",{"type":29,"value":2925}," flag creates each branch at ",{"type":24,"tag":14,"props":2927,"children":2929},{"className":2928},[],[2930],{"type":29,"value":2669},{"type":29,"value":2932}," and checks it out in the new directory. The final ",{"type":24,"tag":14,"props":2934,"children":2936},{"className":2935},[],[2937],{"type":29,"value":2669},{"type":29,"value":2939}," 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":353,"props":2941,"children":2944},{"href":2942,"rel":2943},"https://git-scm.com/docs/git-worktree#_commands",[357],[2945],{"type":29,"value":2946},"Git manual",{"type":29,"value":2948}," describes both ",{"type":24,"tag":14,"props":2950,"children":2952},{"className":2951},[],[2953],{"type":29,"value":2954},"add",{"type":29,"value":2498},{"type":24,"tag":14,"props":2957,"children":2959},{"className":2958},[],[2960],{"type":29,"value":2961},"list",{"type":29,"value":1422},{"type":24,"tag":25,"props":2964,"children":2965},{},[2966,2968,2974,2976,2982],{"type":29,"value":2967},"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":2969,"children":2971},{"className":2970},[],[2972],{"type":29,"value":2973},"search-index",{"type":29,"value":2975},"; “Add API contract tests for the current order response” in ",{"type":24,"tag":14,"props":2977,"children":2979},{"className":2978},[],[2980],{"type":29,"value":2981},"api-tests",{"type":29,"value":2983},". 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":2985,"children":2986},{},[2987],{"type":29,"value":2988},"Move into one directory to work on it:",{"type":24,"tag":206,"props":2990,"children":2992},{"className":208,"code":2991,"language":210,"meta":7,"style":7},"cd ../.worktrees/project/search-index\ngit status --short --branch\n# Edit, run checks, and commit on feature/search-index.\n",[2993],{"type":24,"tag":14,"props":2994,"children":2995},{"__ignoreMap":7},[2996,3008,3029],{"type":24,"tag":216,"props":2997,"children":2998},{"class":218,"line":219},[2999,3003],{"type":24,"tag":216,"props":3000,"children":3001},{"style":245},[3002],{"type":29,"value":1716},{"type":24,"tag":216,"props":3004,"children":3005},{"style":229},[3006],{"type":29,"value":3007}," ../.worktrees/project/search-index\n",{"type":24,"tag":216,"props":3009,"children":3010},{"class":218,"line":251},[3011,3015,3019,3024],{"type":24,"tag":216,"props":3012,"children":3013},{"style":223},[3014],{"type":29,"value":2620},{"type":24,"tag":216,"props":3016,"children":3017},{"style":229},[3018],{"type":29,"value":275},{"type":24,"tag":216,"props":3020,"children":3021},{"style":245},[3022],{"type":29,"value":3023}," --short",{"type":24,"tag":216,"props":3025,"children":3026},{"style":245},[3027],{"type":29,"value":3028}," --branch\n",{"type":24,"tag":216,"props":3030,"children":3031},{"class":218,"line":265},[3032],{"type":24,"tag":216,"props":3033,"children":3035},{"style":3034},"--shiki-default:#6A737D;--shiki-light:#6A737D",[3036],{"type":29,"value":3037},"# Edit, run checks, and commit on feature/search-index.\n",{"type":24,"tag":25,"props":3039,"children":3040},{},[3041,3043,3049,3051,3056],{"type":29,"value":3042},"From that directory, ",{"type":24,"tag":14,"props":3044,"children":3046},{"className":3045},[],[3047],{"type":29,"value":3048},"git status",{"type":29,"value":3050}," describes only that task's checkout. Changes there will not appear in the ",{"type":24,"tag":14,"props":3052,"children":3054},{"className":3053},[],[3055],{"type":29,"value":2981},{"type":29,"value":3057}," 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":3059,"children":3061},{"id":3060},"give-each-checkout-its-own-runtime",[3062],{"type":29,"value":3063},"Give each checkout its own runtime",{"type":24,"tag":25,"props":3065,"children":3066},{},[3067],{"type":29,"value":3068},"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":3070,"children":3071},{},[3072,3074,3080,3082,3087],{"type":29,"value":3073},"For a Node project, install dependencies in each worktree. Do not symlink one ",{"type":24,"tag":14,"props":3075,"children":3077},{"className":3076},[],[3078],{"type":29,"value":3079},"node_modules",{"type":29,"value":3081}," 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":3083,"children":3085},{"className":3084},[],[3086],{"type":29,"value":2761},{"type":29,"value":3088}," for this example:",{"type":24,"tag":206,"props":3090,"children":3092},{"className":208,"code":3091,"language":210,"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",[3093],{"type":24,"tag":14,"props":3094,"children":3095},{"__ignoreMap":7},[3096,3107,3119,3151,3158,3166,3178,3189],{"type":24,"tag":216,"props":3097,"children":3098},{"class":218,"line":219},[3099,3103],{"type":24,"tag":216,"props":3100,"children":3101},{"style":245},[3102],{"type":29,"value":1716},{"type":24,"tag":216,"props":3104,"children":3105},{"style":229},[3106],{"type":29,"value":3007},{"type":24,"tag":216,"props":3108,"children":3109},{"class":218,"line":251},[3110,3114],{"type":24,"tag":216,"props":3111,"children":3112},{"style":223},[3113],{"type":29,"value":1693},{"type":24,"tag":216,"props":3115,"children":3116},{"style":229},[3117],{"type":29,"value":3118}," ci\n",{"type":24,"tag":216,"props":3120,"children":3121},{"class":218,"line":265},[3122,3126,3131,3136,3141,3146],{"type":24,"tag":216,"props":3123,"children":3124},{"style":223},[3125],{"type":29,"value":1693},{"type":24,"tag":216,"props":3127,"children":3128},{"style":229},[3129],{"type":29,"value":3130}," run",{"type":24,"tag":216,"props":3132,"children":3133},{"style":229},[3134],{"type":29,"value":3135}," dev",{"type":24,"tag":216,"props":3137,"children":3138},{"style":245},[3139],{"type":29,"value":3140}," --",{"type":24,"tag":216,"props":3142,"children":3143},{"style":245},[3144],{"type":29,"value":3145}," --port",{"type":24,"tag":216,"props":3147,"children":3148},{"style":245},[3149],{"type":29,"value":3150}," 3101\n",{"type":24,"tag":216,"props":3152,"children":3153},{"class":218,"line":404},[3154],{"type":24,"tag":216,"props":3155,"children":3156},{"emptyLinePlaceholder":390},[3157],{"type":29,"value":393},{"type":24,"tag":216,"props":3159,"children":3160},{"class":218,"line":413},[3161],{"type":24,"tag":216,"props":3162,"children":3163},{"style":3034},[3164],{"type":29,"value":3165},"# In a second terminal:\n",{"type":24,"tag":216,"props":3167,"children":3168},{"class":218,"line":421},[3169,3173],{"type":24,"tag":216,"props":3170,"children":3171},{"style":245},[3172],{"type":29,"value":1716},{"type":24,"tag":216,"props":3174,"children":3175},{"style":229},[3176],{"type":29,"value":3177}," ../.worktrees/project/api-tests\n",{"type":24,"tag":216,"props":3179,"children":3180},{"class":218,"line":430},[3181,3185],{"type":24,"tag":216,"props":3182,"children":3183},{"style":223},[3184],{"type":29,"value":1693},{"type":24,"tag":216,"props":3186,"children":3187},{"style":229},[3188],{"type":29,"value":3118},{"type":24,"tag":216,"props":3190,"children":3191},{"class":218,"line":439},[3192,3196,3200,3204,3208,3212],{"type":24,"tag":216,"props":3193,"children":3194},{"style":223},[3195],{"type":29,"value":1693},{"type":24,"tag":216,"props":3197,"children":3198},{"style":229},[3199],{"type":29,"value":3130},{"type":24,"tag":216,"props":3201,"children":3202},{"style":229},[3203],{"type":29,"value":3135},{"type":24,"tag":216,"props":3205,"children":3206},{"style":245},[3207],{"type":29,"value":3140},{"type":24,"tag":216,"props":3209,"children":3210},{"style":245},[3211],{"type":29,"value":3145},{"type":24,"tag":216,"props":3213,"children":3214},{"style":245},[3215],{"type":29,"value":3216}," 3102\n",{"type":24,"tag":25,"props":3218,"children":3219},{},[3220],{"type":29,"value":3221},"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":3223,"children":3224},{},[3225,3227,3233],{"type":29,"value":3226},"Ignored files are also local to a worktree. A ",{"type":24,"tag":14,"props":3228,"children":3230},{"className":3229},[],[3231],{"type":29,"value":3232},".env.local",{"type":29,"value":3234}," 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":3236,"children":3238},{"id":3237},"one-agent-one-worktree-one-objective",[3239],{"type":29,"value":3240},"One agent, one worktree, one objective",{"type":24,"tag":25,"props":3242,"children":3243},{},[3244],{"type":29,"value":3245},"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":206,"props":3247,"children":3249},{"className":1030,"code":3248,"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",[3250],{"type":24,"tag":14,"props":3251,"children":3252},{"__ignoreMap":7},[3253,3261,3269,3277],{"type":24,"tag":216,"props":3254,"children":3255},{"class":218,"line":219},[3256],{"type":24,"tag":216,"props":3257,"children":3258},{},[3259],{"type":29,"value":3260},"Work only in /workspace/.worktrees/project/search-index.\n",{"type":24,"tag":216,"props":3262,"children":3263},{"class":218,"line":251},[3264],{"type":24,"tag":216,"props":3265,"children":3266},{},[3267],{"type":29,"value":3268},"Goal: improve the order search query without changing the API response.\n",{"type":24,"tag":216,"props":3270,"children":3271},{"class":218,"line":265},[3272],{"type":24,"tag":216,"props":3273,"children":3274},{},[3275],{"type":29,"value":3276},"Run the repository's relevant tests and report the query plan change.\n",{"type":24,"tag":216,"props":3278,"children":3279},{"class":218,"line":404},[3280],{"type":24,"tag":216,"props":3281,"children":3282},{},[3283],{"type":29,"value":3284},"Commit your work on feature/search-index; do not integrate or deploy it.\n",{"type":24,"tag":25,"props":3286,"children":3287},{},[3288],{"type":29,"value":3289},"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":3291,"children":3292},{},[3293],{"type":29,"value":3294},"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":3296,"children":3297},{},[3298],{"type":29,"value":3299},"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":3301,"children":3303},{"id":3302},"integrate-one-result-at-a-time",[3304],{"type":29,"value":3305},"Integrate one result at a time",{"type":24,"tag":25,"props":3307,"children":3308},{},[3309,3311,3316,3318,3323,3325,3330],{"type":29,"value":3310},"Assume ",{"type":24,"tag":14,"props":3312,"children":3314},{"className":3313},[],[3315],{"type":29,"value":2677},{"type":29,"value":3317}," is ready. Starting in ",{"type":24,"tag":14,"props":3319,"children":3321},{"className":3320},[],[3322],{"type":29,"value":2761},{"type":29,"value":3324},", check its diff and verification results in its worktree, then update it onto the latest ",{"type":24,"tag":14,"props":3326,"children":3328},{"className":3327},[],[3329],{"type":29,"value":2669},{"type":29,"value":2175},{"type":24,"tag":206,"props":3332,"children":3334},{"className":208,"code":3333,"language":210,"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",[3335],{"type":24,"tag":14,"props":3336,"children":3337},{"__ignoreMap":7},[3338,3349,3368,3385,3401],{"type":24,"tag":216,"props":3339,"children":3340},{"class":218,"line":219},[3341,3345],{"type":24,"tag":216,"props":3342,"children":3343},{"style":245},[3344],{"type":29,"value":1716},{"type":24,"tag":216,"props":3346,"children":3347},{"style":229},[3348],{"type":29,"value":3007},{"type":24,"tag":216,"props":3350,"children":3351},{"class":218,"line":251},[3352,3356,3360,3364],{"type":24,"tag":216,"props":3353,"children":3354},{"style":223},[3355],{"type":29,"value":2620},{"type":24,"tag":216,"props":3357,"children":3358},{"style":229},[3359],{"type":29,"value":275},{"type":24,"tag":216,"props":3361,"children":3362},{"style":245},[3363],{"type":29,"value":3023},{"type":24,"tag":216,"props":3365,"children":3366},{"style":245},[3367],{"type":29,"value":3028},{"type":24,"tag":216,"props":3369,"children":3370},{"class":218,"line":265},[3371,3375,3380],{"type":24,"tag":216,"props":3372,"children":3373},{"style":223},[3374],{"type":29,"value":2620},{"type":24,"tag":216,"props":3376,"children":3377},{"style":229},[3378],{"type":29,"value":3379}," diff",{"type":24,"tag":216,"props":3381,"children":3382},{"style":229},[3383],{"type":29,"value":3384}," main...HEAD\n",{"type":24,"tag":216,"props":3386,"children":3387},{"class":218,"line":404},[3388,3392,3397],{"type":24,"tag":216,"props":3389,"children":3390},{"style":223},[3391],{"type":29,"value":2620},{"type":24,"tag":216,"props":3393,"children":3394},{"style":229},[3395],{"type":29,"value":3396}," rebase",{"type":24,"tag":216,"props":3398,"children":3399},{"style":229},[3400],{"type":29,"value":2863},{"type":24,"tag":216,"props":3402,"children":3403},{"class":218,"line":413},[3404],{"type":24,"tag":216,"props":3405,"children":3406},{"style":3034},[3407],{"type":29,"value":3408},"# Run the project's required checks again after the rebase.\n",{"type":24,"tag":25,"props":3410,"children":3411},{},[3412,3418,3420,3425,3426,3432],{"type":24,"tag":14,"props":3413,"children":3415},{"className":3414},[],[3416],{"type":29,"value":3417},"git diff main...HEAD",{"type":29,"value":3419}," shows committed changes on the task branch relative to the common ancestor. Review uncommitted changes separately with ",{"type":24,"tag":14,"props":3421,"children":3423},{"className":3422},[],[3424],{"type":29,"value":2295},{"type":29,"value":2498},{"type":24,"tag":14,"props":3427,"children":3429},{"className":3428},[],[3430],{"type":29,"value":3431},"git diff --cached",{"type":29,"value":3433},"; 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":3435,"children":3436},{},[3437],{"type":29,"value":3438},"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":206,"props":3440,"children":3442},{"className":208,"code":3441,"language":210,"meta":7,"style":7},"cd ../../../project\ngit merge --squash feature/search-index\ngit commit -m \"perf: speed up order search\"\n",[3443],{"type":24,"tag":14,"props":3444,"children":3445},{"__ignoreMap":7},[3446,3458,3480],{"type":24,"tag":216,"props":3447,"children":3448},{"class":218,"line":219},[3449,3453],{"type":24,"tag":216,"props":3450,"children":3451},{"style":245},[3452],{"type":29,"value":1716},{"type":24,"tag":216,"props":3454,"children":3455},{"style":229},[3456],{"type":29,"value":3457}," ../../../project\n",{"type":24,"tag":216,"props":3459,"children":3460},{"class":218,"line":251},[3461,3465,3470,3475],{"type":24,"tag":216,"props":3462,"children":3463},{"style":223},[3464],{"type":29,"value":2620},{"type":24,"tag":216,"props":3466,"children":3467},{"style":229},[3468],{"type":29,"value":3469}," merge",{"type":24,"tag":216,"props":3471,"children":3472},{"style":245},[3473],{"type":29,"value":3474}," --squash",{"type":24,"tag":216,"props":3476,"children":3477},{"style":229},[3478],{"type":29,"value":3479}," feature/search-index\n",{"type":24,"tag":216,"props":3481,"children":3482},{"class":218,"line":265},[3483,3487,3492,3497],{"type":24,"tag":216,"props":3484,"children":3485},{"style":223},[3486],{"type":29,"value":2620},{"type":24,"tag":216,"props":3488,"children":3489},{"style":229},[3490],{"type":29,"value":3491}," commit",{"type":24,"tag":216,"props":3493,"children":3494},{"style":245},[3495],{"type":29,"value":3496}," -m",{"type":24,"tag":216,"props":3498,"children":3499},{"style":229},[3500],{"type":29,"value":3501}," \"perf: speed up order search\"\n",{"type":24,"tag":25,"props":3503,"children":3504},{},[3505,3507,3512,3514,3520,3522,3527,3529,3534],{"type":29,"value":3506},"The relative ",{"type":24,"tag":14,"props":3508,"children":3510},{"className":3509},[],[3511],{"type":29,"value":1716},{"type":29,"value":3513}," above assumes the exact layout shown earlier and starts in ",{"type":24,"tag":14,"props":3515,"children":3517},{"className":3516},[],[3518],{"type":29,"value":3519},"search-index/",{"type":29,"value":3521},"; adjust it if you chose another directory. Squash creates one integration commit on ",{"type":24,"tag":14,"props":3523,"children":3525},{"className":3524},[],[3526],{"type":29,"value":2669},{"type":29,"value":3528},". 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":3530,"children":3532},{"className":3531},[],[3533],{"type":29,"value":2669},{"type":29,"value":1422},{"type":24,"tag":25,"props":3536,"children":3537},{},[3538,3540,3545,3547,3552,3554,3559],{"type":29,"value":3539},"Now ",{"type":24,"tag":14,"props":3541,"children":3543},{"className":3542},[],[3544],{"type":29,"value":2685},{"type":29,"value":3546}," still starts from the old ",{"type":24,"tag":14,"props":3548,"children":3550},{"className":3549},[],[3551],{"type":29,"value":2669},{"type":29,"value":3553},". Rebase it, resolve any overlap, and rerun its checks before integrating it. Finally run the combined repository checks on ",{"type":24,"tag":14,"props":3555,"children":3557},{"className":3556},[],[3558],{"type":29,"value":2669},{"type":29,"value":3560},". 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":3562,"children":3564},{"id":3563},"clean-up-without-losing-work",[3565],{"type":29,"value":3566},"Clean up without losing work",{"type":24,"tag":25,"props":3568,"children":3569},{},[3570],{"type":29,"value":3571},"Once a task has been integrated and you have checked that nothing remains uncommitted, retire its checkout:",{"type":24,"tag":206,"props":3573,"children":3575},{"className":208,"code":3574,"language":210,"meta":7,"style":7},"git worktree list\ngit worktree remove ../.worktrees/project/search-index\n",[3576],{"type":24,"tag":14,"props":3577,"children":3578},{"__ignoreMap":7},[3579,3594],{"type":24,"tag":216,"props":3580,"children":3581},{"class":218,"line":219},[3582,3586,3590],{"type":24,"tag":216,"props":3583,"children":3584},{"style":223},[3585],{"type":29,"value":2620},{"type":24,"tag":216,"props":3587,"children":3588},{"style":229},[3589],{"type":29,"value":2838},{"type":24,"tag":216,"props":3591,"children":3592},{"style":229},[3593],{"type":29,"value":2912},{"type":24,"tag":216,"props":3595,"children":3596},{"class":218,"line":251},[3597,3601,3605,3610],{"type":24,"tag":216,"props":3598,"children":3599},{"style":223},[3600],{"type":29,"value":2620},{"type":24,"tag":216,"props":3602,"children":3603},{"style":229},[3604],{"type":29,"value":2838},{"type":24,"tag":216,"props":3606,"children":3607},{"style":229},[3608],{"type":29,"value":3609}," remove",{"type":24,"tag":216,"props":3611,"children":3612},{"style":229},[3613],{"type":29,"value":3007},{"type":24,"tag":25,"props":3615,"children":3616},{},[3617,3619,3624,3626,3632,3634,3640,3642,3647,3649,3655,3657,3663],{"type":29,"value":3618},"Run those commands from the main ",{"type":24,"tag":14,"props":3620,"children":3622},{"className":3621},[],[3623],{"type":29,"value":2761},{"type":29,"value":3625}," checkout. ",{"type":24,"tag":14,"props":3627,"children":3629},{"className":3628},[],[3630],{"type":29,"value":3631},"git worktree remove",{"type":29,"value":3633}," refuses to remove a dirty worktree by default. Do not use ",{"type":24,"tag":14,"props":3635,"children":3637},{"className":3636},[],[3638],{"type":29,"value":3639},"--force",{"type":29,"value":3641}," 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":3643,"children":3645},{"className":3644},[],[3646],{"type":29,"value":2669},{"type":29,"value":3648},"; after verifying the squash contains the intended changes, delete that local branch with ",{"type":24,"tag":14,"props":3650,"children":3652},{"className":3651},[],[3653],{"type":29,"value":3654},"git branch -D feature/search-index",{"type":29,"value":3656},". With a normal merge, ",{"type":24,"tag":14,"props":3658,"children":3660},{"className":3659},[],[3661],{"type":29,"value":3662},"git branch -d",{"type":29,"value":3664}," can check ancestry for you.",{"type":24,"tag":25,"props":3666,"children":3667},{},[3668,3670,3675,3677,3682,3684,3690,3692,3698],{"type":29,"value":3669},"Avoid deleting a worktree directory by hand. Git retains administrative records for linked worktrees; use ",{"type":24,"tag":14,"props":3671,"children":3673},{"className":3672},[],[3674],{"type":29,"value":3631},{"type":29,"value":3676},". If someone has already deleted a directory, inspect ",{"type":24,"tag":14,"props":3678,"children":3680},{"className":3679},[],[3681],{"type":29,"value":2769},{"type":29,"value":3683}," and use ",{"type":24,"tag":14,"props":3685,"children":3687},{"className":3686},[],[3688],{"type":29,"value":3689},"git worktree prune",{"type":29,"value":3691}," only for stale entries. The ",{"type":24,"tag":353,"props":3693,"children":3695},{"href":2942,"rel":3694},[357],[3696],{"type":29,"value":3697},"worktree cleanup documentation",{"type":29,"value":3699}," explains these commands.",{"type":24,"tag":50,"props":3701,"children":3703},{"id":3702},"a-small-operating-checklist",[3704],{"type":29,"value":3705},"A small operating checklist",{"type":24,"tag":25,"props":3707,"children":3708},{},[3709],{"type":29,"value":3710},"The practical habit is simple:",{"type":24,"tag":2531,"props":3712,"children":3713},{},[3714,3719,3724,3729,3734],{"type":24,"tag":1412,"props":3715,"children":3716},{},[3717],{"type":29,"value":3718},"Start each task from a known commit on its own branch and worktree.",{"type":24,"tag":1412,"props":3720,"children":3721},{},[3722],{"type":29,"value":3723},"Assign one person or agent to that worktree and keep the task narrow.",{"type":24,"tag":1412,"props":3725,"children":3726},{},[3727],{"type":29,"value":3728},"Isolate dependencies, ports, caches, and local data that could collide.",{"type":24,"tag":1412,"props":3730,"children":3731},{},[3732],{"type":29,"value":3733},"Review the diff and checks in each branch; integrate sequentially.",{"type":24,"tag":1412,"props":3735,"children":3736},{},[3737],{"type":29,"value":3738},"Verify the combined result and remove finished worktrees.",{"type":24,"tag":25,"props":3740,"children":3741},{},[3742],{"type":29,"value":3743},"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":3745,"children":3746},{"id":1403},[3747],{"type":29,"value":1406},{"type":24,"tag":1408,"props":3749,"children":3750},{},[3751,3761,3772],{"type":24,"tag":1412,"props":3752,"children":3753},{},[3754,3760],{"type":24,"tag":353,"props":3755,"children":3757},{"href":2690,"rel":3756},[357],[3758],{"type":29,"value":3759},"Git: git-worktree documentation",{"type":29,"value":1422},{"type":24,"tag":1412,"props":3762,"children":3763},{},[3764,3771],{"type":24,"tag":353,"props":3765,"children":3768},{"href":3766,"rel":3767},"https://git-scm.com/docs/git-rebase",[357],[3769],{"type":29,"value":3770},"Git: git-rebase documentation",{"type":29,"value":1422},{"type":24,"tag":1412,"props":3773,"children":3774},{},[3775,3782],{"type":24,"tag":353,"props":3776,"children":3779},{"href":3777,"rel":3778},"https://git-scm.com/docs/git-merge",[357],[3780],{"type":29,"value":3781},"Git: git-merge documentation",{"type":29,"value":1422},{"type":24,"tag":1445,"props":3784,"children":3785},{},[3786],{"type":29,"value":1449},{"title":7,"searchDepth":251,"depth":251,"links":3788},[3789,3790,3791,3792,3793,3794,3795,3796],{"id":2647,"depth":251,"text":2650},{"id":2779,"depth":251,"text":2782},{"id":3060,"depth":251,"text":3063},{"id":3237,"depth":251,"text":3240},{"id":3302,"depth":251,"text":3305},{"id":3563,"depth":251,"text":3566},{"id":3702,"depth":251,"text":3705},{"id":1403,"depth":251,"text":1406},"content:posts:git-worktrees-parallel-development.md","posts/git-worktrees-parallel-development.md","posts/git-worktrees-parallel-development",{"_path":3801,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3802,"description":3803,"layout":10,"date":3804,"subtitle":3805,"image":3806,"optimized_image":3806,"category":14,"tags":3807,"author":19,"paginate":6,"body":3811,"_type":372,"_id":5847,"_source":1463,"_file":5848,"_stem":5849,"_extension":1466},"/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,16,3808,3809,3810],"events","postgresql","microservices",{"type":21,"children":3812,"toc":5833},[3813,3826,3831,3841,3846,3859,3865,3870,3893,3898,3903,4002,4015,4029,4043,4049,4054,4059,4064,4069,4075,4080,4143,4164,4169,4455,4490,4736,4755,4768,4797,4809,4815,4820,4825,4954,4966,5046,5051,5071,5076,5081,5087,5092,5205,5217,5222,5228,5233,5339,5374,5551,5564,5569,5574,5580,5585,5606,5611,5625,5631,5656,5661,5666,5671,5677,5682,5687,5692,5697,5702,5708,5713,5746,5751,5757,5762,5767,5772,5776,5829],{"type":24,"tag":25,"props":3814,"children":3815},{},[3816,3818,3824],{"type":29,"value":3817},"Imagine an order service that saves a new order in PostgreSQL and publishes ",{"type":24,"tag":14,"props":3819,"children":3821},{"className":3820},[],[3822],{"type":29,"value":3823},"OrderCreated",{"type":29,"value":3825},". 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":3827,"children":3828},{},[3829],{"type":29,"value":3830},"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":3832,"children":3833},{},[3834,3839],{"type":24,"tag":37,"props":3835,"children":3836},{},[3837],{"type":29,"value":3838},"When a system relies on events to keep its services consistent, publishing those events is part of completing the business operation.",{"type":29,"value":3840}," 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":3842,"children":3843},{},[3844],{"type":29,"value":3845},"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":3847,"children":3848},{},[3849,3851,3857],{"type":29,"value":3850},"The order workflow below is illustrative. It builds on the operational concerns discussed in ",{"type":24,"tag":353,"props":3852,"children":3854},{"href":3853},"/hidden-cost-event-driven-architectures/",[3855],{"type":29,"value":3856},"The Hidden Cost of Event-Driven Architectures",{"type":29,"value":3858},", concentrating on the boundary between a database commit and event publication.",{"type":24,"tag":50,"props":3860,"children":3862},{"id":3861},"two-successful-operations-do-not-make-one-transaction",[3863],{"type":29,"value":3864},"Two successful operations do not make one transaction",{"type":24,"tag":25,"props":3866,"children":3867},{},[3868],{"type":29,"value":3869},"The obvious implementation contains two steps:",{"type":24,"tag":206,"props":3871,"children":3873},{"className":1030,"code":3872,"language":29,"meta":7,"style":7},"save order and commit database transaction\npublish OrderCreated to broker\n",[3874],{"type":24,"tag":14,"props":3875,"children":3876},{"__ignoreMap":7},[3877,3885],{"type":24,"tag":216,"props":3878,"children":3879},{"class":218,"line":219},[3880],{"type":24,"tag":216,"props":3881,"children":3882},{},[3883],{"type":29,"value":3884},"save order and commit database transaction\n",{"type":24,"tag":216,"props":3886,"children":3887},{"class":218,"line":251},[3888],{"type":24,"tag":216,"props":3889,"children":3890},{},[3891],{"type":29,"value":3892},"publish OrderCreated to broker\n",{"type":24,"tag":25,"props":3894,"children":3895},{},[3896],{"type":29,"value":3897},"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":3899,"children":3900},{},[3901],{"type":29,"value":3902},"Changing the order only changes the failure window:",{"type":24,"tag":62,"props":3904,"children":3905},{},[3906,3927],{"type":24,"tag":66,"props":3907,"children":3908},{},[3909],{"type":24,"tag":70,"props":3910,"children":3911},{},[3912,3917,3922],{"type":24,"tag":74,"props":3913,"children":3914},{},[3915],{"type":29,"value":3916},"Sequence",{"type":24,"tag":74,"props":3918,"children":3919},{},[3920],{"type":29,"value":3921},"Failure window",{"type":24,"tag":74,"props":3923,"children":3924},{},[3925],{"type":29,"value":3926},"Result",{"type":24,"tag":85,"props":3928,"children":3929},{},[3930,3948,3966,3984],{"type":24,"tag":70,"props":3931,"children":3932},{},[3933,3938,3943],{"type":24,"tag":92,"props":3934,"children":3935},{},[3936],{"type":29,"value":3937},"Commit the order, then publish",{"type":24,"tag":92,"props":3939,"children":3940},{},[3941],{"type":29,"value":3942},"Process dies after the commit but before publication",{"type":24,"tag":92,"props":3944,"children":3945},{},[3946],{"type":29,"value":3947},"The order exists; its event is missing",{"type":24,"tag":70,"props":3949,"children":3950},{},[3951,3956,3961],{"type":24,"tag":92,"props":3952,"children":3953},{},[3954],{"type":29,"value":3955},"Publish, then commit the order",{"type":24,"tag":92,"props":3957,"children":3958},{},[3959],{"type":29,"value":3960},"Publication succeeds, but the database transaction rolls back",{"type":24,"tag":92,"props":3962,"children":3963},{},[3964],{"type":29,"value":3965},"Consumers receive a fact that was never committed",{"type":24,"tag":70,"props":3967,"children":3968},{},[3969,3974,3979],{"type":24,"tag":92,"props":3970,"children":3971},{},[3972],{"type":29,"value":3973},"Publish while the database transaction is open",{"type":24,"tag":92,"props":3975,"children":3976},{},[3977],{"type":29,"value":3978},"Broker accepts the event; a later database operation or commit fails",{"type":24,"tag":92,"props":3980,"children":3981},{},[3982],{"type":29,"value":3983},"The external side effect survives the database rollback",{"type":24,"tag":70,"props":3985,"children":3986},{},[3987,3992,3997],{"type":24,"tag":92,"props":3988,"children":3989},{},[3990],{"type":29,"value":3991},"Commit, publish, and retry after a timeout",{"type":24,"tag":92,"props":3993,"children":3994},{},[3995],{"type":29,"value":3996},"Broker accepted the message, but its acknowledgement was lost",{"type":24,"tag":92,"props":3998,"children":3999},{},[4000],{"type":29,"value":4001},"Retrying can publish the same event again",{"type":24,"tag":25,"props":4003,"children":4004},{},[4005,4007,4013],{"type":29,"value":4006},"A ",{"type":24,"tag":14,"props":4008,"children":4010},{"className":4009},[],[4011],{"type":29,"value":4012},"try/catch",{"type":29,"value":4014}," 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":4016,"children":4017},{},[4018,4020,4027],{"type":29,"value":4019},"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":353,"props":4021,"children":4024},{"href":4022,"rel":4023},"https://docs.spring.io/spring-framework/reference/data-access/transaction/event.html",[357],[4025],{"type":29,"value":4026},"transaction-bound event listeners",{"type":29,"value":4028}," let you choose a transaction phase; that phase selection alone does not create a persistent delivery mechanism.",{"type":24,"tag":25,"props":4030,"children":4031},{},[4032,4034,4041],{"type":29,"value":4033},"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":353,"props":4035,"children":4038},{"href":4036,"rel":4037},"https://microservices.io/patterns/data/transactional-outbox.html",[357],[4039],{"type":29,"value":4040},"Transactional Outbox pattern",{"type":29,"value":4042}," describes this trade-off.",{"type":24,"tag":50,"props":4044,"children":4046},{"id":4045},"why-a-missing-event-can-become-permanent-inconsistency",[4047],{"type":29,"value":4048},"Why a missing event can become permanent inconsistency",{"type":24,"tag":25,"props":4050,"children":4051},{},[4052],{"type":29,"value":4053},"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":4055,"children":4056},{},[4057],{"type":29,"value":4058},"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":4060,"children":4061},{},[4062],{"type":29,"value":4063},"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":4065,"children":4066},{},[4067],{"type":29,"value":4068},"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":4070,"children":4072},{"id":4071},"commit-the-business-change-and-the-publication-intent-together",[4073],{"type":29,"value":4074},"Commit the business change and the publication intent together",{"type":24,"tag":25,"props":4076,"children":4077},{},[4078],{"type":29,"value":4079},"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":206,"props":4081,"children":4083},{"className":1030,"code":4082,"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",[4084],{"type":24,"tag":14,"props":4085,"children":4086},{"__ignoreMap":7},[4087,4095,4103,4111,4119,4127,4135],{"type":24,"tag":216,"props":4088,"children":4089},{"class":218,"line":219},[4090],{"type":24,"tag":216,"props":4091,"children":4092},{},[4093],{"type":29,"value":4094},"Application transaction in PostgreSQL\n",{"type":24,"tag":216,"props":4096,"children":4097},{"class":218,"line":251},[4098],{"type":24,"tag":216,"props":4099,"children":4100},{},[4101],{"type":29,"value":4102},"    insert order\n",{"type":24,"tag":216,"props":4104,"children":4105},{"class":218,"line":265},[4106],{"type":24,"tag":216,"props":4107,"children":4108},{},[4109],{"type":29,"value":4110},"    insert outbox event\n",{"type":24,"tag":216,"props":4112,"children":4113},{"class":218,"line":404},[4114],{"type":24,"tag":216,"props":4115,"children":4116},{},[4117],{"type":29,"value":4118},"    commit both\n",{"type":24,"tag":216,"props":4120,"children":4121},{"class":218,"line":413},[4122],{"type":24,"tag":216,"props":4123,"children":4124},{},[4125],{"type":29,"value":4126},"            |\n",{"type":24,"tag":216,"props":4128,"children":4129},{"class":218,"line":421},[4130],{"type":24,"tag":216,"props":4131,"children":4132},{},[4133],{"type":29,"value":4134},"            v\n",{"type":24,"tag":216,"props":4136,"children":4137},{"class":218,"line":430},[4138],{"type":24,"tag":216,"props":4139,"children":4140},{},[4141],{"type":29,"value":4142},"Relay reads committed event -> Broker -> Consumers\n",{"type":24,"tag":25,"props":4144,"children":4145},{},[4146,4148,4153,4155,4162],{"type":29,"value":4147},"The atomic boundary is ",{"type":24,"tag":37,"props":4149,"children":4150},{},[4151],{"type":29,"value":4152},"business state plus durable event intent",{"type":29,"value":4154},". It does not include the broker or the consumers. PostgreSQL's ",{"type":24,"tag":353,"props":4156,"children":4159},{"href":4157,"rel":4158},"https://www.postgresql.org/docs/current/tutorial-transactions.html",[357],[4160],{"type":29,"value":4161},"transaction model",{"type":29,"value":4163}," makes the two writes commit or roll back together, provided they actually participate in the same transaction.",{"type":24,"tag":25,"props":4165,"children":4166},{},[4167],{"type":29,"value":4168},"Here is a small schema for a polling implementation:",{"type":24,"tag":206,"props":4170,"children":4174},{"className":4171,"code":4172,"language":4173,"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",[4175],{"type":24,"tag":14,"props":4176,"children":4177},{"__ignoreMap":7},[4178,4203,4221,4243,4263,4284,4304,4325,4342,4369,4382,4390,4397,4414,4427],{"type":24,"tag":216,"props":4179,"children":4180},{"class":218,"line":219},[4181,4187,4192,4197],{"type":24,"tag":216,"props":4182,"children":4184},{"style":4183},"--shiki-default:#F97583;--shiki-light:#D73A49",[4185],{"type":29,"value":4186},"CREATE",{"type":24,"tag":216,"props":4188,"children":4189},{"style":4183},[4190],{"type":29,"value":4191}," TABLE",{"type":24,"tag":216,"props":4193,"children":4194},{"style":223},[4195],{"type":29,"value":4196}," outbox_event",{"type":24,"tag":216,"props":4198,"children":4200},{"style":4199},"--shiki-default:#E1E4E8;--shiki-light:#24292E",[4201],{"type":29,"value":4202}," (\n",{"type":24,"tag":216,"props":4204,"children":4205},{"class":218,"line":251},[4206,4211,4216],{"type":24,"tag":216,"props":4207,"children":4208},{"style":4199},[4209],{"type":29,"value":4210},"    event_id UUID ",{"type":24,"tag":216,"props":4212,"children":4213},{"style":4183},[4214],{"type":29,"value":4215},"PRIMARY KEY",{"type":24,"tag":216,"props":4217,"children":4218},{"style":4199},[4219],{"type":29,"value":4220},",\n",{"type":24,"tag":216,"props":4222,"children":4223},{"class":218,"line":265},[4224,4229,4234,4239],{"type":24,"tag":216,"props":4225,"children":4226},{"style":4199},[4227],{"type":29,"value":4228},"    aggregate_type ",{"type":24,"tag":216,"props":4230,"children":4231},{"style":4183},[4232],{"type":29,"value":4233},"TEXT",{"type":24,"tag":216,"props":4235,"children":4236},{"style":4183},[4237],{"type":29,"value":4238}," NOT NULL",{"type":24,"tag":216,"props":4240,"children":4241},{"style":4199},[4242],{"type":29,"value":4220},{"type":24,"tag":216,"props":4244,"children":4245},{"class":218,"line":404},[4246,4251,4255,4259],{"type":24,"tag":216,"props":4247,"children":4248},{"style":4199},[4249],{"type":29,"value":4250},"    aggregate_id ",{"type":24,"tag":216,"props":4252,"children":4253},{"style":4183},[4254],{"type":29,"value":4233},{"type":24,"tag":216,"props":4256,"children":4257},{"style":4183},[4258],{"type":29,"value":4238},{"type":24,"tag":216,"props":4260,"children":4261},{"style":4199},[4262],{"type":29,"value":4220},{"type":24,"tag":216,"props":4264,"children":4265},{"class":218,"line":413},[4266,4271,4276,4280],{"type":24,"tag":216,"props":4267,"children":4268},{"style":4199},[4269],{"type":29,"value":4270},"    aggregate_version ",{"type":24,"tag":216,"props":4272,"children":4273},{"style":4183},[4274],{"type":29,"value":4275},"BIGINT",{"type":24,"tag":216,"props":4277,"children":4278},{"style":4183},[4279],{"type":29,"value":4238},{"type":24,"tag":216,"props":4281,"children":4282},{"style":4199},[4283],{"type":29,"value":4220},{"type":24,"tag":216,"props":4285,"children":4286},{"class":218,"line":421},[4287,4292,4296,4300],{"type":24,"tag":216,"props":4288,"children":4289},{"style":4199},[4290],{"type":29,"value":4291},"    event_type ",{"type":24,"tag":216,"props":4293,"children":4294},{"style":4183},[4295],{"type":29,"value":4233},{"type":24,"tag":216,"props":4297,"children":4298},{"style":4183},[4299],{"type":29,"value":4238},{"type":24,"tag":216,"props":4301,"children":4302},{"style":4199},[4303],{"type":29,"value":4220},{"type":24,"tag":216,"props":4305,"children":4306},{"class":218,"line":430},[4307,4312,4317,4321],{"type":24,"tag":216,"props":4308,"children":4309},{"style":4199},[4310],{"type":29,"value":4311},"    schema_version ",{"type":24,"tag":216,"props":4313,"children":4314},{"style":4183},[4315],{"type":29,"value":4316},"INTEGER",{"type":24,"tag":216,"props":4318,"children":4319},{"style":4183},[4320],{"type":29,"value":4238},{"type":24,"tag":216,"props":4322,"children":4323},{"style":4199},[4324],{"type":29,"value":4220},{"type":24,"tag":216,"props":4326,"children":4327},{"class":218,"line":439},[4328,4333,4338],{"type":24,"tag":216,"props":4329,"children":4330},{"style":4199},[4331],{"type":29,"value":4332},"    payload JSONB ",{"type":24,"tag":216,"props":4334,"children":4335},{"style":4183},[4336],{"type":29,"value":4337},"NOT NULL",{"type":24,"tag":216,"props":4339,"children":4340},{"style":4199},[4341],{"type":29,"value":4220},{"type":24,"tag":216,"props":4343,"children":4344},{"class":218,"line":448},[4345,4350,4355,4359,4364],{"type":24,"tag":216,"props":4346,"children":4347},{"style":4199},[4348],{"type":29,"value":4349},"    created_at ",{"type":24,"tag":216,"props":4351,"children":4352},{"style":4183},[4353],{"type":29,"value":4354},"TIMESTAMPTZ",{"type":24,"tag":216,"props":4356,"children":4357},{"style":4183},[4358],{"type":29,"value":4238},{"type":24,"tag":216,"props":4360,"children":4361},{"style":4183},[4362],{"type":29,"value":4363}," DEFAULT",{"type":24,"tag":216,"props":4365,"children":4366},{"style":4199},[4367],{"type":29,"value":4368}," CURRENT_TIMESTAMP,\n",{"type":24,"tag":216,"props":4370,"children":4371},{"class":218,"line":457},[4372,4377],{"type":24,"tag":216,"props":4373,"children":4374},{"style":4199},[4375],{"type":29,"value":4376},"    published_at ",{"type":24,"tag":216,"props":4378,"children":4379},{"style":4183},[4380],{"type":29,"value":4381},"TIMESTAMPTZ\n",{"type":24,"tag":216,"props":4383,"children":4384},{"class":218,"line":465},[4385],{"type":24,"tag":216,"props":4386,"children":4387},{"style":4199},[4388],{"type":29,"value":4389},");\n",{"type":24,"tag":216,"props":4391,"children":4392},{"class":218,"line":474},[4393],{"type":24,"tag":216,"props":4394,"children":4395},{"emptyLinePlaceholder":390},[4396],{"type":29,"value":393},{"type":24,"tag":216,"props":4398,"children":4399},{"class":218,"line":483},[4400,4404,4409],{"type":24,"tag":216,"props":4401,"children":4402},{"style":4183},[4403],{"type":29,"value":4186},{"type":24,"tag":216,"props":4405,"children":4406},{"style":4183},[4407],{"type":29,"value":4408}," INDEX",{"type":24,"tag":216,"props":4410,"children":4411},{"style":223},[4412],{"type":29,"value":4413}," outbox_event_pending_idx\n",{"type":24,"tag":216,"props":4415,"children":4416},{"class":218,"line":492},[4417,4422],{"type":24,"tag":216,"props":4418,"children":4419},{"style":4183},[4420],{"type":29,"value":4421},"    ON",{"type":24,"tag":216,"props":4423,"children":4424},{"style":4199},[4425],{"type":29,"value":4426}," outbox_event (created_at, event_id)\n",{"type":24,"tag":216,"props":4428,"children":4429},{"class":218,"line":500},[4430,4435,4440,4445,4450],{"type":24,"tag":216,"props":4431,"children":4432},{"style":4183},[4433],{"type":29,"value":4434},"    WHERE",{"type":24,"tag":216,"props":4436,"children":4437},{"style":4199},[4438],{"type":29,"value":4439}," published_at ",{"type":24,"tag":216,"props":4441,"children":4442},{"style":4183},[4443],{"type":29,"value":4444},"IS",{"type":24,"tag":216,"props":4446,"children":4447},{"style":4183},[4448],{"type":29,"value":4449}," NULL",{"type":24,"tag":216,"props":4451,"children":4452},{"style":4199},[4453],{"type":29,"value":4454},";\n",{"type":24,"tag":25,"props":4456,"children":4457},{},[4458,4459,4465,4467,4473,4475,4480,4482,4488],{"type":29,"value":3310},{"type":24,"tag":14,"props":4460,"children":4462},{"className":4461},[],[4463],{"type":29,"value":4464},"purchase_order",{"type":29,"value":4466}," has an ",{"type":24,"tag":14,"props":4468,"children":4470},{"className":4469},[],[4471],{"type":29,"value":4472},"id",{"type":29,"value":4474}," primary key, a ",{"type":24,"tag":14,"props":4476,"children":4478},{"className":4477},[],[4479],{"type":29,"value":1650},{"type":29,"value":4481},", and a ",{"type":24,"tag":14,"props":4483,"children":4485},{"className":4484},[],[4486],{"type":29,"value":4487},"version",{"type":29,"value":4489},". Creating an order and its event becomes:",{"type":24,"tag":206,"props":4491,"children":4493},{"className":4171,"code":4492,"language":4173,"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",[4494],{"type":24,"tag":14,"props":4495,"children":4496},{"__ignoreMap":7},[4497,4509,4516,4546,4577,4584,4596,4604,4612,4619,4630,4656,4676,4710,4717,4724],{"type":24,"tag":216,"props":4498,"children":4499},{"class":218,"line":219},[4500,4505],{"type":24,"tag":216,"props":4501,"children":4502},{"style":4183},[4503],{"type":29,"value":4504},"BEGIN",{"type":24,"tag":216,"props":4506,"children":4507},{"style":4199},[4508],{"type":29,"value":4454},{"type":24,"tag":216,"props":4510,"children":4511},{"class":218,"line":251},[4512],{"type":24,"tag":216,"props":4513,"children":4514},{"emptyLinePlaceholder":390},[4515],{"type":29,"value":393},{"type":24,"tag":216,"props":4517,"children":4518},{"class":218,"line":265},[4519,4524,4529,4533,4537,4541],{"type":24,"tag":216,"props":4520,"children":4521},{"style":4183},[4522],{"type":29,"value":4523},"INSERT INTO",{"type":24,"tag":216,"props":4525,"children":4526},{"style":4199},[4527],{"type":29,"value":4528}," purchase_order (id, ",{"type":24,"tag":216,"props":4530,"children":4531},{"style":4183},[4532],{"type":29,"value":1650},{"type":24,"tag":216,"props":4534,"children":4535},{"style":4199},[4536],{"type":29,"value":328},{"type":24,"tag":216,"props":4538,"children":4539},{"style":4183},[4540],{"type":29,"value":4487},{"type":24,"tag":216,"props":4542,"children":4543},{"style":4199},[4544],{"type":29,"value":4545},")\n",{"type":24,"tag":216,"props":4547,"children":4548},{"class":218,"line":404},[4549,4554,4559,4564,4568,4573],{"type":24,"tag":216,"props":4550,"children":4551},{"style":4183},[4552],{"type":29,"value":4553},"VALUES",{"type":24,"tag":216,"props":4555,"children":4556},{"style":4199},[4557],{"type":29,"value":4558}," (:order_id, ",{"type":24,"tag":216,"props":4560,"children":4561},{"style":229},[4562],{"type":29,"value":4563},"'CREATED'",{"type":24,"tag":216,"props":4565,"children":4566},{"style":4199},[4567],{"type":29,"value":328},{"type":24,"tag":216,"props":4569,"children":4570},{"style":245},[4571],{"type":29,"value":4572},"1",{"type":24,"tag":216,"props":4574,"children":4575},{"style":4199},[4576],{"type":29,"value":4389},{"type":24,"tag":216,"props":4578,"children":4579},{"class":218,"line":413},[4580],{"type":24,"tag":216,"props":4581,"children":4582},{"emptyLinePlaceholder":390},[4583],{"type":29,"value":393},{"type":24,"tag":216,"props":4585,"children":4586},{"class":218,"line":421},[4587,4591],{"type":24,"tag":216,"props":4588,"children":4589},{"style":4183},[4590],{"type":29,"value":4523},{"type":24,"tag":216,"props":4592,"children":4593},{"style":4199},[4594],{"type":29,"value":4595}," outbox_event (\n",{"type":24,"tag":216,"props":4597,"children":4598},{"class":218,"line":430},[4599],{"type":24,"tag":216,"props":4600,"children":4601},{"style":4199},[4602],{"type":29,"value":4603},"    event_id, aggregate_type, aggregate_id, aggregate_version,\n",{"type":24,"tag":216,"props":4605,"children":4606},{"class":218,"line":439},[4607],{"type":24,"tag":216,"props":4608,"children":4609},{"style":4199},[4610],{"type":29,"value":4611},"    event_type, schema_version, payload\n",{"type":24,"tag":216,"props":4613,"children":4614},{"class":218,"line":448},[4615],{"type":24,"tag":216,"props":4616,"children":4617},{"style":4199},[4618],{"type":29,"value":4545},{"type":24,"tag":216,"props":4620,"children":4621},{"class":218,"line":457},[4622,4626],{"type":24,"tag":216,"props":4623,"children":4624},{"style":4183},[4625],{"type":29,"value":4553},{"type":24,"tag":216,"props":4627,"children":4628},{"style":4199},[4629],{"type":29,"value":4202},{"type":24,"tag":216,"props":4631,"children":4632},{"class":218,"line":465},[4633,4638,4643,4648,4652],{"type":24,"tag":216,"props":4634,"children":4635},{"style":4199},[4636],{"type":29,"value":4637},"    :event_id, ",{"type":24,"tag":216,"props":4639,"children":4640},{"style":229},[4641],{"type":29,"value":4642},"'Order'",{"type":24,"tag":216,"props":4644,"children":4645},{"style":4199},[4646],{"type":29,"value":4647},", :order_id, ",{"type":24,"tag":216,"props":4649,"children":4650},{"style":245},[4651],{"type":29,"value":4572},{"type":24,"tag":216,"props":4653,"children":4654},{"style":4199},[4655],{"type":29,"value":4220},{"type":24,"tag":216,"props":4657,"children":4658},{"class":218,"line":474},[4659,4664,4668,4672],{"type":24,"tag":216,"props":4660,"children":4661},{"style":229},[4662],{"type":29,"value":4663},"    'OrderCreated'",{"type":24,"tag":216,"props":4665,"children":4666},{"style":4199},[4667],{"type":29,"value":328},{"type":24,"tag":216,"props":4669,"children":4670},{"style":245},[4671],{"type":29,"value":4572},{"type":24,"tag":216,"props":4673,"children":4674},{"style":4199},[4675],{"type":29,"value":4220},{"type":24,"tag":216,"props":4677,"children":4678},{"class":218,"line":483},[4679,4684,4689,4693,4698,4702,4706],{"type":24,"tag":216,"props":4680,"children":4681},{"style":4199},[4682],{"type":29,"value":4683},"    jsonb_build_object(",{"type":24,"tag":216,"props":4685,"children":4686},{"style":229},[4687],{"type":29,"value":4688},"'orderId'",{"type":24,"tag":216,"props":4690,"children":4691},{"style":4199},[4692],{"type":29,"value":4647},{"type":24,"tag":216,"props":4694,"children":4695},{"style":229},[4696],{"type":29,"value":4697},"'status'",{"type":24,"tag":216,"props":4699,"children":4700},{"style":4199},[4701],{"type":29,"value":328},{"type":24,"tag":216,"props":4703,"children":4704},{"style":229},[4705],{"type":29,"value":4563},{"type":24,"tag":216,"props":4707,"children":4708},{"style":4199},[4709],{"type":29,"value":4545},{"type":24,"tag":216,"props":4711,"children":4712},{"class":218,"line":492},[4713],{"type":24,"tag":216,"props":4714,"children":4715},{"style":4199},[4716],{"type":29,"value":4389},{"type":24,"tag":216,"props":4718,"children":4719},{"class":218,"line":500},[4720],{"type":24,"tag":216,"props":4721,"children":4722},{"emptyLinePlaceholder":390},[4723],{"type":29,"value":393},{"type":24,"tag":216,"props":4725,"children":4726},{"class":218,"line":509},[4727,4732],{"type":24,"tag":216,"props":4728,"children":4729},{"style":4183},[4730],{"type":29,"value":4731},"COMMIT",{"type":24,"tag":216,"props":4733,"children":4734},{"style":4199},[4735],{"type":29,"value":4454},{"type":24,"tag":25,"props":4737,"children":4738},{},[4739,4740,4746,4747,4753],{"type":29,"value":2917},{"type":24,"tag":14,"props":4741,"children":4743},{"className":4742},[],[4744],{"type":29,"value":4745},":order_id",{"type":29,"value":2498},{"type":24,"tag":14,"props":4748,"children":4750},{"className":4749},[],[4751],{"type":29,"value":4752},":event_id",{"type":29,"value":4754}," 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":4756,"children":4757},{},[4758,4760,4766],{"type":29,"value":4759},"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":4761,"children":4763},{"className":4762},[],[4764],{"type":29,"value":4765},"REQUIRES_NEW",{"type":29,"value":4767}," would defeat the atomic boundary.",{"type":24,"tag":25,"props":4769,"children":4770},{},[4771,4773,4779,4781,4787,4789,4795],{"type":29,"value":4772},"Keep three concepts separate: ",{"type":24,"tag":14,"props":4774,"children":4776},{"className":4775},[],[4777],{"type":29,"value":4778},"event_id",{"type":29,"value":4780}," identifies a particular event, ",{"type":24,"tag":14,"props":4782,"children":4784},{"className":4783},[],[4785],{"type":29,"value":4786},"schema_version",{"type":29,"value":4788}," identifies its payload contract, and ",{"type":24,"tag":14,"props":4790,"children":4792},{"className":4791},[],[4793],{"type":29,"value":4794},"aggregate_version",{"type":29,"value":4796}," 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":4798,"children":4799},{},[4800,4802,4807],{"type":29,"value":4801},"Store an immutable event payload. Loading the latest order and constructing ",{"type":24,"tag":14,"props":4803,"children":4805},{"className":4804},[],[4806],{"type":29,"value":3823},{"type":29,"value":4808}," later can accidentally publish facts from a different transition.",{"type":24,"tag":50,"props":4810,"children":4812},{"id":4811},"publish-from-the-outbox-and-acknowledge-in-the-right-order",[4813],{"type":29,"value":4814},"Publish from the outbox, and acknowledge in the right order",{"type":24,"tag":25,"props":4816,"children":4817},{},[4818],{"type":29,"value":4819},"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":4821,"children":4822},{},[4823],{"type":29,"value":4824},"Start a database transaction and select the row:",{"type":24,"tag":206,"props":4826,"children":4828},{"className":4171,"code":4827,"language":4173,"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",[4829],{"type":24,"tag":14,"props":4830,"children":4831},{"__ignoreMap":7},[4832,4843,4850,4863,4871,4884,4905,4918,4931],{"type":24,"tag":216,"props":4833,"children":4834},{"class":218,"line":219},[4835,4839],{"type":24,"tag":216,"props":4836,"children":4837},{"style":4183},[4838],{"type":29,"value":4504},{"type":24,"tag":216,"props":4840,"children":4841},{"style":4199},[4842],{"type":29,"value":4454},{"type":24,"tag":216,"props":4844,"children":4845},{"class":218,"line":251},[4846],{"type":24,"tag":216,"props":4847,"children":4848},{"emptyLinePlaceholder":390},[4849],{"type":29,"value":393},{"type":24,"tag":216,"props":4851,"children":4852},{"class":218,"line":265},[4853,4858],{"type":24,"tag":216,"props":4854,"children":4855},{"style":4183},[4856],{"type":29,"value":4857},"SELECT",{"type":24,"tag":216,"props":4859,"children":4860},{"style":4199},[4861],{"type":29,"value":4862}," event_id, aggregate_type, aggregate_id, aggregate_version,\n",{"type":24,"tag":216,"props":4864,"children":4865},{"class":218,"line":404},[4866],{"type":24,"tag":216,"props":4867,"children":4868},{"style":4199},[4869],{"type":29,"value":4870},"       event_type, schema_version, payload\n",{"type":24,"tag":216,"props":4872,"children":4873},{"class":218,"line":413},[4874,4879],{"type":24,"tag":216,"props":4875,"children":4876},{"style":4183},[4877],{"type":29,"value":4878},"FROM",{"type":24,"tag":216,"props":4880,"children":4881},{"style":4199},[4882],{"type":29,"value":4883}," outbox_event\n",{"type":24,"tag":216,"props":4885,"children":4886},{"class":218,"line":421},[4887,4892,4896,4900],{"type":24,"tag":216,"props":4888,"children":4889},{"style":4183},[4890],{"type":29,"value":4891},"WHERE",{"type":24,"tag":216,"props":4893,"children":4894},{"style":4199},[4895],{"type":29,"value":4439},{"type":24,"tag":216,"props":4897,"children":4898},{"style":4183},[4899],{"type":29,"value":4444},{"type":24,"tag":216,"props":4901,"children":4902},{"style":4183},[4903],{"type":29,"value":4904}," NULL\n",{"type":24,"tag":216,"props":4906,"children":4907},{"class":218,"line":430},[4908,4913],{"type":24,"tag":216,"props":4909,"children":4910},{"style":4183},[4911],{"type":29,"value":4912},"ORDER BY",{"type":24,"tag":216,"props":4914,"children":4915},{"style":4199},[4916],{"type":29,"value":4917}," created_at, event_id\n",{"type":24,"tag":216,"props":4919,"children":4920},{"class":218,"line":439},[4921,4926],{"type":24,"tag":216,"props":4922,"children":4923},{"style":4183},[4924],{"type":29,"value":4925},"LIMIT",{"type":24,"tag":216,"props":4927,"children":4928},{"style":245},[4929],{"type":29,"value":4930}," 1\n",{"type":24,"tag":216,"props":4932,"children":4933},{"class":218,"line":448},[4934,4939,4944,4949],{"type":24,"tag":216,"props":4935,"children":4936},{"style":4183},[4937],{"type":29,"value":4938},"FOR",{"type":24,"tag":216,"props":4940,"children":4941},{"style":4183},[4942],{"type":29,"value":4943}," UPDATE",{"type":24,"tag":216,"props":4945,"children":4946},{"style":4183},[4947],{"type":29,"value":4948}," SKIP",{"type":24,"tag":216,"props":4950,"children":4951},{"style":4199},[4952],{"type":29,"value":4953}," LOCKED;\n",{"type":24,"tag":25,"props":4955,"children":4956},{},[4957,4959,4964],{"type":29,"value":4958},"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":4960,"children":4962},{"className":4961},[],[4963],{"type":29,"value":4778},{"type":29,"value":4965},". Only after the broker acknowledges acceptance under the required durability settings should the same transaction run:",{"type":24,"tag":206,"props":4967,"children":4969},{"className":4171,"code":4968,"language":4173,"meta":7,"style":7},"UPDATE outbox_event\nSET published_at = CURRENT_TIMESTAMP\nWHERE event_id = :event_id;\n\nCOMMIT;\n",[4970],{"type":24,"tag":14,"props":4971,"children":4972},{"__ignoreMap":7},[4973,4985,5007,5028,5035],{"type":24,"tag":216,"props":4974,"children":4975},{"class":218,"line":219},[4976,4981],{"type":24,"tag":216,"props":4977,"children":4978},{"style":4183},[4979],{"type":29,"value":4980},"UPDATE",{"type":24,"tag":216,"props":4982,"children":4983},{"style":4199},[4984],{"type":29,"value":4883},{"type":24,"tag":216,"props":4986,"children":4987},{"class":218,"line":251},[4988,4993,4997,5002],{"type":24,"tag":216,"props":4989,"children":4990},{"style":4183},[4991],{"type":29,"value":4992},"SET",{"type":24,"tag":216,"props":4994,"children":4995},{"style":4199},[4996],{"type":29,"value":4439},{"type":24,"tag":216,"props":4998,"children":4999},{"style":4183},[5000],{"type":29,"value":5001},"=",{"type":24,"tag":216,"props":5003,"children":5004},{"style":4199},[5005],{"type":29,"value":5006}," CURRENT_TIMESTAMP\n",{"type":24,"tag":216,"props":5008,"children":5009},{"class":218,"line":265},[5010,5014,5019,5023],{"type":24,"tag":216,"props":5011,"children":5012},{"style":4183},[5013],{"type":29,"value":4891},{"type":24,"tag":216,"props":5015,"children":5016},{"style":4199},[5017],{"type":29,"value":5018}," event_id ",{"type":24,"tag":216,"props":5020,"children":5021},{"style":4183},[5022],{"type":29,"value":5001},{"type":24,"tag":216,"props":5024,"children":5025},{"style":4199},[5026],{"type":29,"value":5027}," :event_id;\n",{"type":24,"tag":216,"props":5029,"children":5030},{"class":218,"line":404},[5031],{"type":24,"tag":216,"props":5032,"children":5033},{"emptyLinePlaceholder":390},[5034],{"type":29,"value":393},{"type":24,"tag":216,"props":5036,"children":5037},{"class":218,"line":413},[5038,5042],{"type":24,"tag":216,"props":5039,"children":5040},{"style":4183},[5041],{"type":29,"value":4731},{"type":24,"tag":216,"props":5043,"children":5044},{"style":4199},[5045],{"type":29,"value":4454},{"type":24,"tag":25,"props":5047,"children":5048},{},[5049],{"type":29,"value":5050},"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":5052,"children":5053},{},[5054,5060,5062,5069],{"type":24,"tag":14,"props":5055,"children":5057},{"className":5056},[],[5058],{"type":29,"value":5059},"SKIP LOCKED",{"type":29,"value":5061}," lets concurrent workers bypass rows already locked by other workers. PostgreSQL documents its usefulness for ",{"type":24,"tag":353,"props":5063,"children":5066},{"href":5064,"rel":5065},"https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE",[357],[5067],{"type":29,"value":5068},"queue-like tables",{"type":29,"value":5070},". It is a work-claiming mechanism, not an ordering guarantee.",{"type":24,"tag":25,"props":5072,"children":5073},{},[5074],{"type":29,"value":5075},"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":5077,"children":5078},{},[5079],{"type":29,"value":5080},"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":5082,"children":5084},{"id":5083},"the-remaining-crash-window-produces-duplicates",[5085],{"type":29,"value":5086},"The remaining crash window produces duplicates",{"type":24,"tag":25,"props":5088,"children":5089},{},[5090],{"type":29,"value":5091},"The relay still talks to two systems. The important difference is that its work is durable and repeatable:",{"type":24,"tag":62,"props":5093,"children":5094},{},[5095,5116],{"type":24,"tag":66,"props":5096,"children":5097},{},[5098],{"type":24,"tag":70,"props":5099,"children":5100},{},[5101,5106,5111],{"type":24,"tag":74,"props":5102,"children":5103},{},[5104],{"type":29,"value":5105},"Failure point",{"type":24,"tag":74,"props":5107,"children":5108},{},[5109],{"type":29,"value":5110},"Durable state",{"type":24,"tag":74,"props":5112,"children":5113},{},[5114],{"type":29,"value":5115},"Recovery",{"type":24,"tag":85,"props":5117,"children":5118},{},[5119,5137,5155,5181],{"type":24,"tag":70,"props":5120,"children":5121},{},[5122,5127,5132],{"type":24,"tag":92,"props":5123,"children":5124},{},[5125],{"type":29,"value":5126},"Before the producer transaction commits",{"type":24,"tag":92,"props":5128,"children":5129},{},[5130],{"type":29,"value":5131},"Neither order nor outbox event committed",{"type":24,"tag":92,"props":5133,"children":5134},{},[5135],{"type":29,"value":5136},"Retry the command according to its idempotency policy",{"type":24,"tag":70,"props":5138,"children":5139},{},[5140,5145,5150],{"type":24,"tag":92,"props":5141,"children":5142},{},[5143],{"type":29,"value":5144},"After that commit, before publication",{"type":24,"tag":92,"props":5146,"children":5147},{},[5148],{"type":29,"value":5149},"Order and pending event exist",{"type":24,"tag":92,"props":5151,"children":5152},{},[5153],{"type":29,"value":5154},"Relay publishes after recovery",{"type":24,"tag":70,"props":5156,"children":5157},{},[5158,5171,5176],{"type":24,"tag":92,"props":5159,"children":5160},{},[5161,5163,5169],{"type":29,"value":5162},"After broker acceptance, before ",{"type":24,"tag":14,"props":5164,"children":5166},{"className":5165},[],[5167],{"type":29,"value":5168},"published_at",{"type":29,"value":5170}," commits",{"type":24,"tag":92,"props":5172,"children":5173},{},[5174],{"type":29,"value":5175},"Broker may have the event; outbox still appears pending",{"type":24,"tag":92,"props":5177,"children":5178},{},[5179],{"type":29,"value":5180},"Relay republishes the same event ID",{"type":24,"tag":70,"props":5182,"children":5183},{},[5184,5195,5200],{"type":24,"tag":92,"props":5185,"children":5186},{},[5187,5189,5194],{"type":29,"value":5188},"After ",{"type":24,"tag":14,"props":5190,"children":5192},{"className":5191},[],[5193],{"type":29,"value":5168},{"type":29,"value":5170},{"type":24,"tag":92,"props":5196,"children":5197},{},[5198],{"type":29,"value":5199},"Broker has acknowledged; relay recorded success",{"type":24,"tag":92,"props":5201,"children":5202},{},[5203],{"type":29,"value":5204},"Normal consumer delivery continues",{"type":24,"tag":25,"props":5206,"children":5207},{},[5208,5210,5215],{"type":29,"value":5209},"This is why outbox-based publication is normally designed around ",{"type":24,"tag":37,"props":5211,"children":5212},{},[5213],{"type":29,"value":5214},"at-least-once delivery",{"type":29,"value":5216},". The pattern closes the missing-intent window; it does not promise a single delivery across database, broker, and consumers.",{"type":24,"tag":25,"props":5218,"children":5219},{},[5220],{"type":29,"value":5221},"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":5223,"children":5225},{"id":5224},"make-the-consumers-effect-safe-to-repeat",[5226],{"type":29,"value":5227},"Make the consumer's effect safe to repeat",{"type":24,"tag":25,"props":5229,"children":5230},{},[5231],{"type":29,"value":5232},"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":206,"props":5234,"children":5236},{"className":4171,"code":5235,"language":4173,"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",[5237],{"type":24,"tag":14,"props":5238,"children":5239},{"__ignoreMap":7},[5240,5260,5280,5295,5319,5332],{"type":24,"tag":216,"props":5241,"children":5242},{"class":218,"line":219},[5243,5247,5251,5256],{"type":24,"tag":216,"props":5244,"children":5245},{"style":4183},[5246],{"type":29,"value":4186},{"type":24,"tag":216,"props":5248,"children":5249},{"style":4183},[5250],{"type":29,"value":4191},{"type":24,"tag":216,"props":5252,"children":5253},{"style":223},[5254],{"type":29,"value":5255}," processed_event",{"type":24,"tag":216,"props":5257,"children":5258},{"style":4199},[5259],{"type":29,"value":4202},{"type":24,"tag":216,"props":5261,"children":5262},{"class":218,"line":251},[5263,5268,5272,5276],{"type":24,"tag":216,"props":5264,"children":5265},{"style":4199},[5266],{"type":29,"value":5267},"    consumer_name ",{"type":24,"tag":216,"props":5269,"children":5270},{"style":4183},[5271],{"type":29,"value":4233},{"type":24,"tag":216,"props":5273,"children":5274},{"style":4183},[5275],{"type":29,"value":4238},{"type":24,"tag":216,"props":5277,"children":5278},{"style":4199},[5279],{"type":29,"value":4220},{"type":24,"tag":216,"props":5281,"children":5282},{"class":218,"line":265},[5283,5287,5291],{"type":24,"tag":216,"props":5284,"children":5285},{"style":4199},[5286],{"type":29,"value":4210},{"type":24,"tag":216,"props":5288,"children":5289},{"style":4183},[5290],{"type":29,"value":4337},{"type":24,"tag":216,"props":5292,"children":5293},{"style":4199},[5294],{"type":29,"value":4220},{"type":24,"tag":216,"props":5296,"children":5297},{"class":218,"line":404},[5298,5303,5307,5311,5315],{"type":24,"tag":216,"props":5299,"children":5300},{"style":4199},[5301],{"type":29,"value":5302},"    processed_at ",{"type":24,"tag":216,"props":5304,"children":5305},{"style":4183},[5306],{"type":29,"value":4354},{"type":24,"tag":216,"props":5308,"children":5309},{"style":4183},[5310],{"type":29,"value":4238},{"type":24,"tag":216,"props":5312,"children":5313},{"style":4183},[5314],{"type":29,"value":4363},{"type":24,"tag":216,"props":5316,"children":5317},{"style":4199},[5318],{"type":29,"value":4368},{"type":24,"tag":216,"props":5320,"children":5321},{"class":218,"line":413},[5322,5327],{"type":24,"tag":216,"props":5323,"children":5324},{"style":4183},[5325],{"type":29,"value":5326},"    PRIMARY KEY",{"type":24,"tag":216,"props":5328,"children":5329},{"style":4199},[5330],{"type":29,"value":5331}," (consumer_name, event_id)\n",{"type":24,"tag":216,"props":5333,"children":5334},{"class":218,"line":421},[5335],{"type":24,"tag":216,"props":5336,"children":5337},{"style":4199},[5338],{"type":29,"value":4389},{"type":24,"tag":25,"props":5340,"children":5341},{},[5342,5344,5349,5351,5357,5359,5365,5367,5372],{"type":29,"value":5343},"For the ",{"type":24,"tag":14,"props":5345,"children":5347},{"className":5346},[],[5348],{"type":29,"value":3823},{"type":29,"value":5350}," event, assume ",{"type":24,"tag":14,"props":5352,"children":5354},{"className":5353},[],[5355],{"type":29,"value":5356},"order_projection",{"type":29,"value":5358}," contains an ",{"type":24,"tag":14,"props":5360,"children":5362},{"className":5361},[],[5363],{"type":29,"value":5364},"order_id",{"type":29,"value":5366}," primary key and a ",{"type":24,"tag":14,"props":5368,"children":5370},{"className":5369},[],[5371],{"type":29,"value":1650},{"type":29,"value":5373},". This statement gates the projection insert on whether the event is new to this consumer:",{"type":24,"tag":206,"props":5375,"children":5377},{"className":4171,"code":5376,"language":4173,"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",[5378],{"type":24,"tag":14,"props":5379,"children":5380},{"__ignoreMap":7},[5381,5392,5399,5421,5434,5457,5469,5477,5484,5504,5521,5533,5540],{"type":24,"tag":216,"props":5382,"children":5383},{"class":218,"line":219},[5384,5388],{"type":24,"tag":216,"props":5385,"children":5386},{"style":4183},[5387],{"type":29,"value":4504},{"type":24,"tag":216,"props":5389,"children":5390},{"style":4199},[5391],{"type":29,"value":4454},{"type":24,"tag":216,"props":5393,"children":5394},{"class":218,"line":251},[5395],{"type":24,"tag":216,"props":5396,"children":5397},{"emptyLinePlaceholder":390},[5398],{"type":29,"value":393},{"type":24,"tag":216,"props":5400,"children":5401},{"class":218,"line":265},[5402,5407,5412,5417],{"type":24,"tag":216,"props":5403,"children":5404},{"style":4183},[5405],{"type":29,"value":5406},"WITH",{"type":24,"tag":216,"props":5408,"children":5409},{"style":4199},[5410],{"type":29,"value":5411}," first_delivery ",{"type":24,"tag":216,"props":5413,"children":5414},{"style":4183},[5415],{"type":29,"value":5416},"AS",{"type":24,"tag":216,"props":5418,"children":5419},{"style":4199},[5420],{"type":29,"value":4202},{"type":24,"tag":216,"props":5422,"children":5423},{"class":218,"line":404},[5424,5429],{"type":24,"tag":216,"props":5425,"children":5426},{"style":4183},[5427],{"type":29,"value":5428},"    INSERT INTO",{"type":24,"tag":216,"props":5430,"children":5431},{"style":4199},[5432],{"type":29,"value":5433}," processed_event (consumer_name, event_id)\n",{"type":24,"tag":216,"props":5435,"children":5436},{"class":218,"line":413},[5437,5442,5447,5452],{"type":24,"tag":216,"props":5438,"children":5439},{"style":4183},[5440],{"type":29,"value":5441},"    VALUES",{"type":24,"tag":216,"props":5443,"children":5444},{"style":4199},[5445],{"type":29,"value":5446}," (",{"type":24,"tag":216,"props":5448,"children":5449},{"style":229},[5450],{"type":29,"value":5451},"'order-search'",{"type":24,"tag":216,"props":5453,"children":5454},{"style":4199},[5455],{"type":29,"value":5456},", :event_id)\n",{"type":24,"tag":216,"props":5458,"children":5459},{"class":218,"line":421},[5460,5464],{"type":24,"tag":216,"props":5461,"children":5462},{"style":4183},[5463],{"type":29,"value":4421},{"type":24,"tag":216,"props":5465,"children":5466},{"style":4199},[5467],{"type":29,"value":5468}," CONFLICT (consumer_name, event_id) DO NOTHING\n",{"type":24,"tag":216,"props":5470,"children":5471},{"class":218,"line":430},[5472],{"type":24,"tag":216,"props":5473,"children":5474},{"style":4199},[5475],{"type":29,"value":5476},"    RETURNING event_id\n",{"type":24,"tag":216,"props":5478,"children":5479},{"class":218,"line":439},[5480],{"type":24,"tag":216,"props":5481,"children":5482},{"style":4199},[5483],{"type":29,"value":4545},{"type":24,"tag":216,"props":5485,"children":5486},{"class":218,"line":448},[5487,5491,5496,5500],{"type":24,"tag":216,"props":5488,"children":5489},{"style":4183},[5490],{"type":29,"value":4523},{"type":24,"tag":216,"props":5492,"children":5493},{"style":4199},[5494],{"type":29,"value":5495}," order_projection (order_id, ",{"type":24,"tag":216,"props":5497,"children":5498},{"style":4183},[5499],{"type":29,"value":1650},{"type":24,"tag":216,"props":5501,"children":5502},{"style":4199},[5503],{"type":29,"value":4545},{"type":24,"tag":216,"props":5505,"children":5506},{"class":218,"line":457},[5507,5511,5516],{"type":24,"tag":216,"props":5508,"children":5509},{"style":4183},[5510],{"type":29,"value":4857},{"type":24,"tag":216,"props":5512,"children":5513},{"style":4199},[5514],{"type":29,"value":5515}," :order_id, ",{"type":24,"tag":216,"props":5517,"children":5518},{"style":229},[5519],{"type":29,"value":5520},"'CREATED'\n",{"type":24,"tag":216,"props":5522,"children":5523},{"class":218,"line":465},[5524,5528],{"type":24,"tag":216,"props":5525,"children":5526},{"style":4183},[5527],{"type":29,"value":4878},{"type":24,"tag":216,"props":5529,"children":5530},{"style":4199},[5531],{"type":29,"value":5532}," first_delivery;\n",{"type":24,"tag":216,"props":5534,"children":5535},{"class":218,"line":474},[5536],{"type":24,"tag":216,"props":5537,"children":5538},{"emptyLinePlaceholder":390},[5539],{"type":29,"value":393},{"type":24,"tag":216,"props":5541,"children":5542},{"class":218,"line":483},[5543,5547],{"type":24,"tag":216,"props":5544,"children":5545},{"style":4183},[5546],{"type":29,"value":4731},{"type":24,"tag":216,"props":5548,"children":5549},{"style":4199},[5550],{"type":29,"value":4454},{"type":24,"tag":25,"props":5552,"children":5553},{},[5554,5556,5563],{"type":29,"value":5555},"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":353,"props":5557,"children":5560},{"href":5558,"rel":5559},"https://microservices.io/patterns/communication-style/idempotent-consumer.html",[357],[5561],{"type":29,"value":5562},"Idempotent Consumer pattern",{"type":29,"value":1422},{"type":24,"tag":25,"props":5565,"children":5566},{},[5567],{"type":29,"value":5568},"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":5570,"children":5571},{},[5572],{"type":29,"value":5573},"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":5575,"children":5577},{"id":5576},"choose-polling-or-cdc-deliberately",[5578],{"type":29,"value":5579},"Choose polling or CDC deliberately",{"type":24,"tag":25,"props":5581,"children":5582},{},[5583],{"type":29,"value":5584},"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":5586,"children":5587},{},[5588,5590,5597,5599,5604],{"type":29,"value":5589},"Change data capture is another way to implement the relay. Debezium's ",{"type":24,"tag":353,"props":5591,"children":5594},{"href":5592,"rel":5593},"https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html",[357],[5595],{"type":29,"value":5596},"Outbox Event Router",{"type":29,"value":5598}," 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":5600,"children":5602},{"className":5601},[],[5603],{"type":29,"value":3823},{"type":29,"value":5605}," contract.",{"type":24,"tag":25,"props":5607,"children":5608},{},[5609],{"type":29,"value":5610},"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":5612,"children":5613},{},[5614,5616,5623],{"type":29,"value":5615},"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":353,"props":5617,"children":5620},{"href":5618,"rel":5619},"https://debezium.io/documentation/reference/stable/connectors/postgresql.html",[357],[5621],{"type":29,"value":5622},"PostgreSQL connector documentation",{"type":29,"value":5624}," describes those responsibilities. Do not assume changing the relay transport eliminates end-to-end duplicates or consumer idempotency requirements.",{"type":24,"tag":50,"props":5626,"children":5628},{"id":5627},"ordering-needs-a-separate-design",[5629],{"type":29,"value":5630},"Ordering needs a separate design",{"type":24,"tag":25,"props":5632,"children":5633},{},[5634,5639,5641,5646,5648,5654],{"type":24,"tag":14,"props":5635,"children":5637},{"className":5636},[],[5638],{"type":29,"value":3823},{"type":29,"value":5640}," followed by ",{"type":24,"tag":14,"props":5642,"children":5644},{"className":5643},[],[5645],{"type":29,"value":167},{"type":29,"value":5647}," 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":5649,"children":5651},{"className":5650},[],[5652],{"type":29,"value":5653},"created_at",{"type":29,"value":5655}," or a generated ID does not establish commit order across concurrent transactions either.",{"type":24,"tag":25,"props":5657,"children":5658},{},[5659],{"type":29,"value":5660},"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":5662,"children":5663},{},[5664],{"type":29,"value":5665},"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":5667,"children":5668},{},[5669],{"type":29,"value":5670},"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":5672,"children":5674},{"id":5673},"operate-the-pending-work-as-part-of-the-product",[5675],{"type":29,"value":5676},"Operate the pending work as part of the product",{"type":24,"tag":25,"props":5678,"children":5679},{},[5680],{"type":29,"value":5681},"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":5683,"children":5684},{},[5685],{"type":29,"value":5686},"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":5688,"children":5689},{},[5690],{"type":29,"value":5691},"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":5693,"children":5694},{},[5695],{"type":29,"value":5696},"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":5698,"children":5699},{},[5700],{"type":29,"value":5701},"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":5703,"children":5705},{"id":5704},"test-by-stopping-the-system-at-inconvenient-moments",[5706],{"type":29,"value":5707},"Test by stopping the system at inconvenient moments",{"type":24,"tag":25,"props":5709,"children":5710},{},[5711],{"type":29,"value":5712},"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":1408,"props":5714,"children":5715},{},[5716,5721,5726,5731,5736,5741],{"type":24,"tag":1412,"props":5717,"children":5718},{},[5719],{"type":29,"value":5720},"Fail the outbox insert and confirm that the business row does not commit.",{"type":24,"tag":1412,"props":5722,"children":5723},{},[5724],{"type":29,"value":5725},"Terminate the producer after its commit and confirm that the relay later publishes the pending event.",{"type":24,"tag":1412,"props":5727,"children":5728},{},[5729],{"type":29,"value":5730},"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":1412,"props":5732,"children":5733},{},[5734],{"type":29,"value":5735},"Stop the consumer after its database commit but before acknowledgement. Confirm the duplicate is recognized.",{"type":24,"tag":1412,"props":5737,"children":5738},{},[5739],{"type":29,"value":5740},"Keep the broker unavailable, observe the backlog and alert, then restore it and verify recovery without deleting pending work.",{"type":24,"tag":1412,"props":5742,"children":5743},{},[5744],{"type":29,"value":5745},"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":5747,"children":5748},{},[5749],{"type":29,"value":5750},"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":5752,"children":5754},{"id":5753},"events-that-maintain-consistency-need-durable-publication",[5755],{"type":29,"value":5756},"Events that maintain consistency need durable publication",{"type":24,"tag":25,"props":5758,"children":5759},{},[5760],{"type":29,"value":5761},"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":5763,"children":5764},{},[5765],{"type":29,"value":5766},"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":5768,"children":5769},{},[5770],{"type":29,"value":5771},"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":5773,"children":5774},{"id":1403},[5775],{"type":29,"value":1406},{"type":24,"tag":1408,"props":5777,"children":5778},{},[5779,5789,5799,5809,5819],{"type":24,"tag":1412,"props":5780,"children":5781},{},[5782,5788],{"type":24,"tag":353,"props":5783,"children":5785},{"href":4036,"rel":5784},[357],[5786],{"type":29,"value":5787},"Chris Richardson: Transactional Outbox",{"type":29,"value":1422},{"type":24,"tag":1412,"props":5790,"children":5791},{},[5792,5798],{"type":24,"tag":353,"props":5793,"children":5795},{"href":5558,"rel":5794},[357],[5796],{"type":29,"value":5797},"Chris Richardson: Idempotent Consumer",{"type":29,"value":1422},{"type":24,"tag":1412,"props":5800,"children":5801},{},[5802,5808],{"type":24,"tag":353,"props":5803,"children":5805},{"href":4157,"rel":5804},[357],[5806],{"type":29,"value":5807},"PostgreSQL: transactions",{"type":29,"value":1422},{"type":24,"tag":1412,"props":5810,"children":5811},{},[5812,5818],{"type":24,"tag":353,"props":5813,"children":5815},{"href":5592,"rel":5814},[357],[5816],{"type":29,"value":5817},"Debezium: Outbox Event Router",{"type":29,"value":1422},{"type":24,"tag":1412,"props":5820,"children":5821},{},[5822,5828],{"type":24,"tag":353,"props":5823,"children":5825},{"href":5618,"rel":5824},[357],[5826],{"type":29,"value":5827},"Debezium: PostgreSQL connector",{"type":29,"value":1422},{"type":24,"tag":1445,"props":5830,"children":5831},{},[5832],{"type":29,"value":1449},{"title":7,"searchDepth":251,"depth":251,"links":5834},[5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846],{"id":3861,"depth":251,"text":3864},{"id":4045,"depth":251,"text":4048},{"id":4071,"depth":251,"text":4074},{"id":4811,"depth":251,"text":4814},{"id":5083,"depth":251,"text":5086},{"id":5224,"depth":251,"text":5227},{"id":5576,"depth":251,"text":5579},{"id":5627,"depth":251,"text":5630},{"id":5673,"depth":251,"text":5676},{"id":5704,"depth":251,"text":5707},{"id":5753,"depth":251,"text":5756},{"id":1403,"depth":251,"text":1406},"content:posts:transactional-outbox.md","posts/transactional-outbox.md","posts/transactional-outbox",[5851,5852,5853,5854,5855,5859,5863,5867,5870,5874,5878,5882,5886,5890,5894,5898,5902,5906,5910,5914,5918,5922,5926,5930,5934,5938,5941,5945,5949],{"_path":1469,"title":1470,"date":1472},{"_path":4,"title":8,"date":11},{"_path":2613,"title":2614,"date":2616},{"_path":3801,"title":3802,"date":3804},{"_path":5856,"title":5857,"date":5858},"/posts/api-evolution-strategy","Your API Needs an Evolution Strategy","2025-05-15T11:00:00.000Z",{"_path":5860,"title":5861,"date":5862},"/posts/n-plus-one-queries","The N+1 Query Problem: Finding and Fixing a Slow Search Endpoint","2025-04-15T11:00:00.000Z",{"_path":5864,"title":5865,"date":5866},"/posts/relational-vs-nosql-databases","Relational vs. NoSQL Databases: Why PostgreSQL Is My Default","2025-03-15T11:00:00.000Z",{"_path":5868,"title":3856,"date":5869},"/posts/hidden-cost-event-driven-architectures","2025-02-15T11:00:00.000Z",{"_path":5871,"title":5872,"date":5873},"/posts/gpt-sql-integration","Natural Language to SQL with GPT: Build a Safer Query Interface","2025-01-15T11:00:00.000Z",{"_path":5875,"title":5876,"date":5877},"/posts/java-security-best-practices","Java Security Best Practices: Preventing Vulnerabilities and Threats","2024-08-15T11:00:00.000Z",{"_path":5879,"title":5880,"date":5881},"/posts/circuit-breaking-resilience","Circuit Breaking in Distributed Systems","2024-07-15T11:00:00.000Z",{"_path":5883,"title":5884,"date":5885},"/posts/maven-and-github-actions","Automating Java Builds with Maven and GitHub Actions CI/CD","2024-06-15T11:00:00.000Z",{"_path":5887,"title":5888,"date":5889},"/posts/pattern-matching-sealed-classes-java21","A Guide to Pattern Matching and Sealed Classes in Java 21","2024-05-15T11:00:00.000Z",{"_path":5891,"title":5892,"date":5893},"/posts/async-java-completable-vs-reactive","Asynchronous Programming in Java: CompletableFuture vs Reactive Streams","2024-04-15T11:00:00.000Z",{"_path":5895,"title":5896,"date":5897},"/posts/mastering-concurrency","Mastering Concurrency in Java: Threads, Executors, and Virtual Threads","2024-03-15T11:00:00.000Z",{"_path":5899,"title":5900,"date":5901},"/posts/microservices-mistakes","Common Mistakes When Developing Microservices","2024-02-15T11:00:00.000Z",{"_path":5903,"title":5904,"date":5905},"/posts/tdd-testing","Test-Driven Development (TDD) in Java with JUnit and Mockito","2024-01-15T11:00:00.000Z",{"_path":5907,"title":5908,"date":5909},"/posts/single-table-design","Single Table Design","2023-12-27T11:00:00.000Z",{"_path":5911,"title":5912,"date":5913},"/posts/java-17","Exploring Java 17's New Features","2023-10-08T11:00:00.000Z",{"_path":5915,"title":5916,"date":5917},"/posts/hexagonal-architecture","Introduction to Hexagonal Architecture","2023-10-02 18:00:00",{"_path":5919,"title":5920,"date":5921},"/posts/terraform","Unleashing the Power of Infrastructure as Code (IaC)","2023-09-24 18:00:00",{"_path":5923,"title":5924,"date":5925},"/posts/python-virtual-environments","Python Virtual Environments (venv)","2023-09-17 22:00:00",{"_path":5927,"title":5928,"date":5929},"/posts/api-first-principles","API First principles","2023-09-01 22:15:18",{"_path":5931,"title":5932,"date":5933},"/posts/how-to-configure-ssh-key-based-authentication","How To Configure SSH Key-Based Authentication","2021-02-21 03:46:12",{"_path":5935,"title":5936,"date":5937},"/posts/algorithms-binary-search-tree","Algorithms - Binary Search Tree in kotlin","2020-12-06T11:00:00.000Z",{"_path":5939,"title":5940,"date":5937},"/posts/how-to-create-a-twitter-bot","How to create a Twitter bot",{"_path":5942,"title":5943,"date":5944},"/posts/manage-aws-infrastructure-with-terraform","Bootstrap complete Java application infrastructure in AWS with Terraform","2020-12-02T11:00:00.000Z",{"_path":5946,"title":5947,"date":5948},"/posts/algorithms-working-with-trees","Algorithms - Binary tree traversals in kotlin","2020-11-24T11:00:00.000Z",{"_path":5950,"title":5951,"date":5952},"/posts/playing-with-ocr-libraries","Playing with OCR libraries","2020-11-22T11:00:00.000Z",1791264529629]