[{"data":1,"prerenderedAt":4551},["ShallowReactive",2],{"post-mastering-concurrency":3,"rel-mastering-concurrency":1336,"sib-mastering-concurrency":4443},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"layout":11,"subtitle":12,"image":13,"optimized_image":13,"category":14,"tags":15,"author":19,"paginate":6,"body":20,"_type":1330,"_id":1331,"_source":1332,"_file":1333,"_stem":1334,"_extension":1335},"/posts/mastering-concurrency","posts",false,"","Mastering Concurrency in Java: Threads, Executors, and Virtual Threads","Exploring Java’s concurrency model with detailed insights into Threads, ExecutorService, and Virtual Threads from Project Loom.","2024-03-15T11:00:00.000Z","post","A Deep Dive into Java’s Multithreading and Parallel Computing","/assets/img/uploads/java-threads-executors-virtual.jpg","code",[14,16,17,18],"java","concurrency","multithreading","jaimedearcos",{"type":21,"children":22,"toc":1313},"root",[23,35,42,47,103,116,122,150,432,439,472,477,483,503,509,514,815,821,868,874,892,898,903,909,942,947,1212,1218,1236,1242,1247,1252,1258,1302,1307],{"type":24,"tag":25,"props":26,"children":27},"element","blockquote",{},[28],{"type":24,"tag":29,"props":30,"children":31},"p",{},[32],{"type":33,"value":34},"text","Java's concurrency model is a critical component for building scalable and efficient applications. From classic Thread management to modern ExecutorService and the revolutionary Virtual Threads in Project Loom, mastering Java’s concurrency tools is essential for high-performance development.",{"type":24,"tag":36,"props":37,"children":39},"h2",{"id":38},"understanding-javas-concurrency-model",[40],{"type":33,"value":41},"Understanding Java’s Concurrency Model",{"type":24,"tag":29,"props":43,"children":44},{},[45],{"type":33,"value":46},"Concurrency in Java enables applications to perform multiple tasks simultaneously, improving throughput and responsiveness. Java provides several concurrency primitives:",{"type":24,"tag":48,"props":49,"children":50},"ul",{},[51,63,73,83,93],{"type":24,"tag":52,"props":53,"children":54},"li",{},[55,61],{"type":24,"tag":56,"props":57,"children":58},"strong",{},[59],{"type":33,"value":60},"Threads",{"type":33,"value":62},": The fundamental unit of execution, allowing tasks to run in parallel.",{"type":24,"tag":52,"props":64,"children":65},{},[66,71],{"type":24,"tag":56,"props":67,"children":68},{},[69],{"type":33,"value":70},"Executors",{"type":33,"value":72},": High-level abstraction for managing thread pools and task execution.",{"type":24,"tag":52,"props":74,"children":75},{},[76,81],{"type":24,"tag":56,"props":77,"children":78},{},[79],{"type":33,"value":80},"Virtual Threads",{"type":33,"value":82},": Lightweight, memory-efficient threads introduced in Java 21 for massive scalability.",{"type":24,"tag":52,"props":84,"children":85},{},[86,91],{"type":24,"tag":56,"props":87,"children":88},{},[89],{"type":33,"value":90},"Fork/Join Framework",{"type":33,"value":92},": Designed for divide-and-conquer algorithms using parallel task execution.",{"type":24,"tag":52,"props":94,"children":95},{},[96,101],{"type":24,"tag":56,"props":97,"children":98},{},[99],{"type":33,"value":100},"Reactive Programming",{"type":33,"value":102},": Asynchronous, non-blocking programming using event streams.",{"type":24,"tag":29,"props":104,"children":105},{},[106,108,114],{"type":33,"value":107},"Let's now take a deeper look at three major building blocks of concurrent programming in Java: classic threads, the ",{"type":24,"tag":14,"props":109,"children":111},{"className":110},[],[112],{"type":33,"value":113},"ExecutorService",{"type":33,"value":115},", and the new virtual threads.",{"type":24,"tag":36,"props":117,"children":119},{"id":118},"classic-approach-java-threads",[120],{"type":33,"value":121},"Classic Approach: Java Threads",{"type":24,"tag":29,"props":123,"children":124},{},[125,127,133,135,140,142,148],{"type":33,"value":126},"The most basic unit of concurrency in Java is the ",{"type":24,"tag":14,"props":128,"children":130},{"className":129},[],[131],{"type":33,"value":132},"Thread",{"type":33,"value":134},". You can extend the ",{"type":24,"tag":14,"props":136,"children":138},{"className":137},[],[139],{"type":33,"value":132},{"type":33,"value":141}," class or implement the ",{"type":24,"tag":14,"props":143,"children":145},{"className":144},[],[146],{"type":33,"value":147},"Runnable",{"type":33,"value":149}," interface to define concurrent tasks.",{"type":24,"tag":151,"props":152,"children":155},"pre",{"className":153,"code":154,"language":16,"meta":7,"style":7},"language-java shiki shiki-themes github-dark github-light","class SimpleThreadExample extends Thread {\n    public void run() {\n        System.out.println(\"Thread running: \" + Thread.currentThread().getName());\n    }\n}\n\npublic class ThreadDemo {\n    public static void main(String[] args) {\n        SimpleThreadExample thread = new SimpleThreadExample();\n        thread.start();\n    }\n}\n",[156],{"type":24,"tag":14,"props":157,"children":158},{"__ignoreMap":7},[159,193,217,272,281,290,300,323,370,398,416,424],{"type":24,"tag":160,"props":161,"children":164},"span",{"class":162,"line":163},"line",1,[165,171,177,182,187],{"type":24,"tag":160,"props":166,"children":168},{"style":167},"--shiki-default:#F97583;--shiki-light:#D73A49",[169],{"type":33,"value":170},"class",{"type":24,"tag":160,"props":172,"children":174},{"style":173},"--shiki-default:#B392F0;--shiki-light:#6F42C1",[175],{"type":33,"value":176}," SimpleThreadExample",{"type":24,"tag":160,"props":178,"children":179},{"style":167},[180],{"type":33,"value":181}," extends",{"type":24,"tag":160,"props":183,"children":184},{"style":173},[185],{"type":33,"value":186}," Thread",{"type":24,"tag":160,"props":188,"children":190},{"style":189},"--shiki-default:#E1E4E8;--shiki-light:#24292E",[191],{"type":33,"value":192}," {\n",{"type":24,"tag":160,"props":194,"children":196},{"class":162,"line":195},2,[197,202,207,212],{"type":24,"tag":160,"props":198,"children":199},{"style":167},[200],{"type":33,"value":201},"    public",{"type":24,"tag":160,"props":203,"children":204},{"style":167},[205],{"type":33,"value":206}," void",{"type":24,"tag":160,"props":208,"children":209},{"style":173},[210],{"type":33,"value":211}," run",{"type":24,"tag":160,"props":213,"children":214},{"style":189},[215],{"type":33,"value":216},"() {\n",{"type":24,"tag":160,"props":218,"children":220},{"class":162,"line":219},3,[221,226,231,236,242,247,252,257,262,267],{"type":24,"tag":160,"props":222,"children":223},{"style":189},[224],{"type":33,"value":225},"        System.out.",{"type":24,"tag":160,"props":227,"children":228},{"style":173},[229],{"type":33,"value":230},"println",{"type":24,"tag":160,"props":232,"children":233},{"style":189},[234],{"type":33,"value":235},"(",{"type":24,"tag":160,"props":237,"children":239},{"style":238},"--shiki-default:#9ECBFF;--shiki-light:#032F62",[240],{"type":33,"value":241},"\"Thread running: \"",{"type":24,"tag":160,"props":243,"children":244},{"style":167},[245],{"type":33,"value":246}," +",{"type":24,"tag":160,"props":248,"children":249},{"style":189},[250],{"type":33,"value":251}," Thread.",{"type":24,"tag":160,"props":253,"children":254},{"style":173},[255],{"type":33,"value":256},"currentThread",{"type":24,"tag":160,"props":258,"children":259},{"style":189},[260],{"type":33,"value":261},"().",{"type":24,"tag":160,"props":263,"children":264},{"style":173},[265],{"type":33,"value":266},"getName",{"type":24,"tag":160,"props":268,"children":269},{"style":189},[270],{"type":33,"value":271},"());\n",{"type":24,"tag":160,"props":273,"children":275},{"class":162,"line":274},4,[276],{"type":24,"tag":160,"props":277,"children":278},{"style":189},[279],{"type":33,"value":280},"    }\n",{"type":24,"tag":160,"props":282,"children":284},{"class":162,"line":283},5,[285],{"type":24,"tag":160,"props":286,"children":287},{"style":189},[288],{"type":33,"value":289},"}\n",{"type":24,"tag":160,"props":291,"children":293},{"class":162,"line":292},6,[294],{"type":24,"tag":160,"props":295,"children":297},{"emptyLinePlaceholder":296},true,[298],{"type":33,"value":299},"\n",{"type":24,"tag":160,"props":301,"children":303},{"class":162,"line":302},7,[304,309,314,319],{"type":24,"tag":160,"props":305,"children":306},{"style":167},[307],{"type":33,"value":308},"public",{"type":24,"tag":160,"props":310,"children":311},{"style":167},[312],{"type":33,"value":313}," class",{"type":24,"tag":160,"props":315,"children":316},{"style":173},[317],{"type":33,"value":318}," ThreadDemo",{"type":24,"tag":160,"props":320,"children":321},{"style":189},[322],{"type":33,"value":192},{"type":24,"tag":160,"props":324,"children":326},{"class":162,"line":325},8,[327,331,336,340,345,349,354,359,365],{"type":24,"tag":160,"props":328,"children":329},{"style":167},[330],{"type":33,"value":201},{"type":24,"tag":160,"props":332,"children":333},{"style":167},[334],{"type":33,"value":335}," static",{"type":24,"tag":160,"props":337,"children":338},{"style":167},[339],{"type":33,"value":206},{"type":24,"tag":160,"props":341,"children":342},{"style":173},[343],{"type":33,"value":344}," main",{"type":24,"tag":160,"props":346,"children":347},{"style":189},[348],{"type":33,"value":235},{"type":24,"tag":160,"props":350,"children":351},{"style":167},[352],{"type":33,"value":353},"String",{"type":24,"tag":160,"props":355,"children":356},{"style":189},[357],{"type":33,"value":358},"[] ",{"type":24,"tag":160,"props":360,"children":362},{"style":361},"--shiki-default:#FFAB70;--shiki-light:#E36209",[363],{"type":33,"value":364},"args",{"type":24,"tag":160,"props":366,"children":367},{"style":189},[368],{"type":33,"value":369},") {\n",{"type":24,"tag":160,"props":371,"children":373},{"class":162,"line":372},9,[374,379,384,389,393],{"type":24,"tag":160,"props":375,"children":376},{"style":189},[377],{"type":33,"value":378},"        SimpleThreadExample thread ",{"type":24,"tag":160,"props":380,"children":381},{"style":167},[382],{"type":33,"value":383},"=",{"type":24,"tag":160,"props":385,"children":386},{"style":167},[387],{"type":33,"value":388}," new",{"type":24,"tag":160,"props":390,"children":391},{"style":173},[392],{"type":33,"value":176},{"type":24,"tag":160,"props":394,"children":395},{"style":189},[396],{"type":33,"value":397},"();\n",{"type":24,"tag":160,"props":399,"children":401},{"class":162,"line":400},10,[402,407,412],{"type":24,"tag":160,"props":403,"children":404},{"style":189},[405],{"type":33,"value":406},"        thread.",{"type":24,"tag":160,"props":408,"children":409},{"style":173},[410],{"type":33,"value":411},"start",{"type":24,"tag":160,"props":413,"children":414},{"style":189},[415],{"type":33,"value":397},{"type":24,"tag":160,"props":417,"children":419},{"class":162,"line":418},11,[420],{"type":24,"tag":160,"props":421,"children":422},{"style":189},[423],{"type":33,"value":280},{"type":24,"tag":160,"props":425,"children":427},{"class":162,"line":426},12,[428],{"type":24,"tag":160,"props":429,"children":430},{"style":189},[431],{"type":33,"value":289},{"type":24,"tag":433,"props":434,"children":436},"h3",{"id":435},"limitations-of-raw-threads",[437],{"type":33,"value":438},"Limitations of Raw Threads",{"type":24,"tag":48,"props":440,"children":441},{},[442,452,462],{"type":24,"tag":52,"props":443,"children":444},{},[445,450],{"type":24,"tag":56,"props":446,"children":447},{},[448],{"type":33,"value":449},"Performance Overhead",{"type":33,"value":451},": Creating and destroying threads is expensive.",{"type":24,"tag":52,"props":453,"children":454},{},[455,460],{"type":24,"tag":56,"props":456,"children":457},{},[458],{"type":33,"value":459},"No Pooling",{"type":33,"value":461},": Each thread is a one-off. No built-in reuse.",{"type":24,"tag":52,"props":463,"children":464},{},[465,470],{"type":24,"tag":56,"props":466,"children":467},{},[468],{"type":33,"value":469},"Scalability Issues",{"type":33,"value":471},": System resources limit the number of threads you can create.",{"type":24,"tag":29,"props":473,"children":474},{},[475],{"type":33,"value":476},"Using raw threads for large-scale concurrent applications quickly becomes impractical.",{"type":24,"tag":36,"props":478,"children":480},{"id":479},"modern-concurrency-executorservice-and-thread-pooling",[481],{"type":33,"value":482},"Modern Concurrency: ExecutorService and Thread Pooling",{"type":24,"tag":29,"props":484,"children":485},{},[486,488,493,495,501],{"type":33,"value":487},"To solve the limitations of raw threads, Java introduced the ",{"type":24,"tag":14,"props":489,"children":491},{"className":490},[],[492],{"type":33,"value":113},{"type":33,"value":494}," as part of ",{"type":24,"tag":14,"props":496,"children":498},{"className":497},[],[499],{"type":33,"value":500},"java.util.concurrent",{"type":33,"value":502},". It provides a high-level API to manage and reuse threads through pooling.",{"type":24,"tag":433,"props":504,"children":506},{"id":505},"how-thread-pooling-works",[507],{"type":33,"value":508},"How Thread Pooling Works",{"type":24,"tag":29,"props":510,"children":511},{},[512],{"type":33,"value":513},"Thread pooling allows you to reuse a fixed number of threads to execute multiple tasks. Instead of creating a new thread for each task, the executor assigns tasks to available worker threads in the pool.",{"type":24,"tag":151,"props":515,"children":517},{"className":153,"code":516,"language":16,"meta":7,"style":7},"import java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class ExecutorExample {\n    public static void main(String[] args) {\n        ExecutorService executor = Executors.newFixedThreadPool(3);\n        for (int i = 0; i \u003C 5; i++) {\n            executor.submit(() -> System.out.println(\"Task executed by: \" + Thread.currentThread().getName()));\n        }\n        executor.shutdown();\n    }\n}\n",[518],{"type":24,"tag":14,"props":519,"children":520},{"__ignoreMap":7},[521,534,546,553,573,612,649,710,776,784,801,808],{"type":24,"tag":160,"props":522,"children":523},{"class":162,"line":163},[524,529],{"type":24,"tag":160,"props":525,"children":526},{"style":167},[527],{"type":33,"value":528},"import",{"type":24,"tag":160,"props":530,"children":531},{"style":189},[532],{"type":33,"value":533}," java.util.concurrent.ExecutorService;\n",{"type":24,"tag":160,"props":535,"children":536},{"class":162,"line":195},[537,541],{"type":24,"tag":160,"props":538,"children":539},{"style":167},[540],{"type":33,"value":528},{"type":24,"tag":160,"props":542,"children":543},{"style":189},[544],{"type":33,"value":545}," java.util.concurrent.Executors;\n",{"type":24,"tag":160,"props":547,"children":548},{"class":162,"line":219},[549],{"type":24,"tag":160,"props":550,"children":551},{"emptyLinePlaceholder":296},[552],{"type":33,"value":299},{"type":24,"tag":160,"props":554,"children":555},{"class":162,"line":274},[556,560,564,569],{"type":24,"tag":160,"props":557,"children":558},{"style":167},[559],{"type":33,"value":308},{"type":24,"tag":160,"props":561,"children":562},{"style":167},[563],{"type":33,"value":313},{"type":24,"tag":160,"props":565,"children":566},{"style":173},[567],{"type":33,"value":568}," ExecutorExample",{"type":24,"tag":160,"props":570,"children":571},{"style":189},[572],{"type":33,"value":192},{"type":24,"tag":160,"props":574,"children":575},{"class":162,"line":283},[576,580,584,588,592,596,600,604,608],{"type":24,"tag":160,"props":577,"children":578},{"style":167},[579],{"type":33,"value":201},{"type":24,"tag":160,"props":581,"children":582},{"style":167},[583],{"type":33,"value":335},{"type":24,"tag":160,"props":585,"children":586},{"style":167},[587],{"type":33,"value":206},{"type":24,"tag":160,"props":589,"children":590},{"style":173},[591],{"type":33,"value":344},{"type":24,"tag":160,"props":593,"children":594},{"style":189},[595],{"type":33,"value":235},{"type":24,"tag":160,"props":597,"children":598},{"style":167},[599],{"type":33,"value":353},{"type":24,"tag":160,"props":601,"children":602},{"style":189},[603],{"type":33,"value":358},{"type":24,"tag":160,"props":605,"children":606},{"style":361},[607],{"type":33,"value":364},{"type":24,"tag":160,"props":609,"children":610},{"style":189},[611],{"type":33,"value":369},{"type":24,"tag":160,"props":613,"children":614},{"class":162,"line":292},[615,620,624,629,634,638,644],{"type":24,"tag":160,"props":616,"children":617},{"style":189},[618],{"type":33,"value":619},"        ExecutorService executor ",{"type":24,"tag":160,"props":621,"children":622},{"style":167},[623],{"type":33,"value":383},{"type":24,"tag":160,"props":625,"children":626},{"style":189},[627],{"type":33,"value":628}," Executors.",{"type":24,"tag":160,"props":630,"children":631},{"style":173},[632],{"type":33,"value":633},"newFixedThreadPool",{"type":24,"tag":160,"props":635,"children":636},{"style":189},[637],{"type":33,"value":235},{"type":24,"tag":160,"props":639,"children":641},{"style":640},"--shiki-default:#79B8FF;--shiki-light:#005CC5",[642],{"type":33,"value":643},"3",{"type":24,"tag":160,"props":645,"children":646},{"style":189},[647],{"type":33,"value":648},");\n",{"type":24,"tag":160,"props":650,"children":651},{"class":162,"line":302},[652,657,662,667,672,676,681,686,691,696,701,706],{"type":24,"tag":160,"props":653,"children":654},{"style":167},[655],{"type":33,"value":656},"        for",{"type":24,"tag":160,"props":658,"children":659},{"style":189},[660],{"type":33,"value":661}," (",{"type":24,"tag":160,"props":663,"children":664},{"style":167},[665],{"type":33,"value":666},"int",{"type":24,"tag":160,"props":668,"children":669},{"style":189},[670],{"type":33,"value":671}," i ",{"type":24,"tag":160,"props":673,"children":674},{"style":167},[675],{"type":33,"value":383},{"type":24,"tag":160,"props":677,"children":678},{"style":640},[679],{"type":33,"value":680}," 0",{"type":24,"tag":160,"props":682,"children":683},{"style":189},[684],{"type":33,"value":685},"; i ",{"type":24,"tag":160,"props":687,"children":688},{"style":167},[689],{"type":33,"value":690},"\u003C",{"type":24,"tag":160,"props":692,"children":693},{"style":640},[694],{"type":33,"value":695}," 5",{"type":24,"tag":160,"props":697,"children":698},{"style":189},[699],{"type":33,"value":700},"; i",{"type":24,"tag":160,"props":702,"children":703},{"style":167},[704],{"type":33,"value":705},"++",{"type":24,"tag":160,"props":707,"children":708},{"style":189},[709],{"type":33,"value":369},{"type":24,"tag":160,"props":711,"children":712},{"class":162,"line":325},[713,718,723,728,733,738,742,746,751,755,759,763,767,771],{"type":24,"tag":160,"props":714,"children":715},{"style":189},[716],{"type":33,"value":717},"            executor.",{"type":24,"tag":160,"props":719,"children":720},{"style":173},[721],{"type":33,"value":722},"submit",{"type":24,"tag":160,"props":724,"children":725},{"style":189},[726],{"type":33,"value":727},"(() ",{"type":24,"tag":160,"props":729,"children":730},{"style":167},[731],{"type":33,"value":732},"->",{"type":24,"tag":160,"props":734,"children":735},{"style":189},[736],{"type":33,"value":737}," System.out.",{"type":24,"tag":160,"props":739,"children":740},{"style":173},[741],{"type":33,"value":230},{"type":24,"tag":160,"props":743,"children":744},{"style":189},[745],{"type":33,"value":235},{"type":24,"tag":160,"props":747,"children":748},{"style":238},[749],{"type":33,"value":750},"\"Task executed by: \"",{"type":24,"tag":160,"props":752,"children":753},{"style":167},[754],{"type":33,"value":246},{"type":24,"tag":160,"props":756,"children":757},{"style":189},[758],{"type":33,"value":251},{"type":24,"tag":160,"props":760,"children":761},{"style":173},[762],{"type":33,"value":256},{"type":24,"tag":160,"props":764,"children":765},{"style":189},[766],{"type":33,"value":261},{"type":24,"tag":160,"props":768,"children":769},{"style":173},[770],{"type":33,"value":266},{"type":24,"tag":160,"props":772,"children":773},{"style":189},[774],{"type":33,"value":775},"()));\n",{"type":24,"tag":160,"props":777,"children":778},{"class":162,"line":372},[779],{"type":24,"tag":160,"props":780,"children":781},{"style":189},[782],{"type":33,"value":783},"        }\n",{"type":24,"tag":160,"props":785,"children":786},{"class":162,"line":400},[787,792,797],{"type":24,"tag":160,"props":788,"children":789},{"style":189},[790],{"type":33,"value":791},"        executor.",{"type":24,"tag":160,"props":793,"children":794},{"style":173},[795],{"type":33,"value":796},"shutdown",{"type":24,"tag":160,"props":798,"children":799},{"style":189},[800],{"type":33,"value":397},{"type":24,"tag":160,"props":802,"children":803},{"class":162,"line":418},[804],{"type":24,"tag":160,"props":805,"children":806},{"style":189},[807],{"type":33,"value":280},{"type":24,"tag":160,"props":809,"children":810},{"class":162,"line":426},[811],{"type":24,"tag":160,"props":812,"children":813},{"style":189},[814],{"type":33,"value":289},{"type":24,"tag":433,"props":816,"children":818},{"id":817},"common-executor-types",[819],{"type":33,"value":820},"Common Executor Types:",{"type":24,"tag":48,"props":822,"children":823},{},[824,835,846,857],{"type":24,"tag":52,"props":825,"children":826},{},[827,833],{"type":24,"tag":14,"props":828,"children":830},{"className":829},[],[831],{"type":33,"value":832},"newFixedThreadPool(n)",{"type":33,"value":834},": Fixed number of threads.",{"type":24,"tag":52,"props":836,"children":837},{},[838,844],{"type":24,"tag":14,"props":839,"children":841},{"className":840},[],[842],{"type":33,"value":843},"newCachedThreadPool()",{"type":33,"value":845},": Dynamically creates threads as needed.",{"type":24,"tag":52,"props":847,"children":848},{},[849,855],{"type":24,"tag":14,"props":850,"children":852},{"className":851},[],[853],{"type":33,"value":854},"newSingleThreadExecutor()",{"type":33,"value":856},": One thread for all tasks.",{"type":24,"tag":52,"props":858,"children":859},{},[860,866],{"type":24,"tag":14,"props":861,"children":863},{"className":862},[],[864],{"type":33,"value":865},"newScheduledThreadPool(n)",{"type":33,"value":867},": Supports delayed and periodic tasks.",{"type":24,"tag":433,"props":869,"children":871},{"id":870},"advantages",[872],{"type":33,"value":873},"Advantages:",{"type":24,"tag":48,"props":875,"children":876},{},[877,882,887],{"type":24,"tag":52,"props":878,"children":879},{},[880],{"type":33,"value":881},"Efficient resource use through reuse.",{"type":24,"tag":52,"props":883,"children":884},{},[885],{"type":33,"value":886},"Better scalability and responsiveness.",{"type":24,"tag":52,"props":888,"children":889},{},[890],{"type":33,"value":891},"Built-in management of task queues and error handling.",{"type":24,"tag":36,"props":893,"children":895},{"id":894},"the-future-virtual-threads-and-project-loom",[896],{"type":33,"value":897},"The Future: Virtual Threads and Project Loom",{"type":24,"tag":29,"props":899,"children":900},{},[901],{"type":33,"value":902},"Java 21 introduces Virtual Threads, a lightweight and scalable alternative to platform threads. A virtual thread is managed by the Java runtime rather than the operating system, allowing millions of threads to coexist.",{"type":24,"tag":433,"props":904,"children":906},{"id":905},"key-features",[907],{"type":33,"value":908},"Key Features:",{"type":24,"tag":48,"props":910,"children":911},{},[912,922,932],{"type":24,"tag":52,"props":913,"children":914},{},[915,920],{"type":24,"tag":56,"props":916,"children":917},{},[918],{"type":33,"value":919},"Minimal Memory Footprint",{"type":33,"value":921},": Much lighter than traditional threads.",{"type":24,"tag":52,"props":923,"children":924},{},[925,930],{"type":24,"tag":56,"props":926,"children":927},{},[928],{"type":33,"value":929},"Unblocked I/O",{"type":33,"value":931},": I/O operations don’t block the underlying platform thread.",{"type":24,"tag":52,"props":933,"children":934},{},[935,940],{"type":24,"tag":56,"props":936,"children":937},{},[938],{"type":33,"value":939},"Massive Scalability",{"type":33,"value":941},": Suitable for applications requiring thousands or millions of concurrent tasks (e.g., web servers).",{"type":24,"tag":29,"props":943,"children":944},{},[945],{"type":33,"value":946},"Example:",{"type":24,"tag":151,"props":948,"children":950},{"className":153,"code":949,"language":16,"meta":7,"style":7},"public class VirtualThreadExample {\n    public static void main(String[] args) {\n        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n            for (int i = 0; i \u003C 10; i++) {\n                executor.submit(() -> {\n                    System.out.println(\"Running in: \" + Thread.currentThread().getName());\n                });\n            }\n        }\n    }\n}\n",[951],{"type":24,"tag":14,"props":952,"children":953},{"__ignoreMap":7},[954,974,1013,1053,1106,1130,1175,1183,1191,1198,1205],{"type":24,"tag":160,"props":955,"children":956},{"class":162,"line":163},[957,961,965,970],{"type":24,"tag":160,"props":958,"children":959},{"style":167},[960],{"type":33,"value":308},{"type":24,"tag":160,"props":962,"children":963},{"style":167},[964],{"type":33,"value":313},{"type":24,"tag":160,"props":966,"children":967},{"style":173},[968],{"type":33,"value":969}," VirtualThreadExample",{"type":24,"tag":160,"props":971,"children":972},{"style":189},[973],{"type":33,"value":192},{"type":24,"tag":160,"props":975,"children":976},{"class":162,"line":195},[977,981,985,989,993,997,1001,1005,1009],{"type":24,"tag":160,"props":978,"children":979},{"style":167},[980],{"type":33,"value":201},{"type":24,"tag":160,"props":982,"children":983},{"style":167},[984],{"type":33,"value":335},{"type":24,"tag":160,"props":986,"children":987},{"style":167},[988],{"type":33,"value":206},{"type":24,"tag":160,"props":990,"children":991},{"style":173},[992],{"type":33,"value":344},{"type":24,"tag":160,"props":994,"children":995},{"style":189},[996],{"type":33,"value":235},{"type":24,"tag":160,"props":998,"children":999},{"style":167},[1000],{"type":33,"value":353},{"type":24,"tag":160,"props":1002,"children":1003},{"style":189},[1004],{"type":33,"value":358},{"type":24,"tag":160,"props":1006,"children":1007},{"style":361},[1008],{"type":33,"value":364},{"type":24,"tag":160,"props":1010,"children":1011},{"style":189},[1012],{"type":33,"value":369},{"type":24,"tag":160,"props":1014,"children":1015},{"class":162,"line":219},[1016,1021,1025,1030,1035,1039,1043,1048],{"type":24,"tag":160,"props":1017,"children":1018},{"style":167},[1019],{"type":33,"value":1020},"        try",{"type":24,"tag":160,"props":1022,"children":1023},{"style":189},[1024],{"type":33,"value":661},{"type":24,"tag":160,"props":1026,"children":1027},{"style":167},[1028],{"type":33,"value":1029},"var",{"type":24,"tag":160,"props":1031,"children":1032},{"style":189},[1033],{"type":33,"value":1034}," executor ",{"type":24,"tag":160,"props":1036,"children":1037},{"style":167},[1038],{"type":33,"value":383},{"type":24,"tag":160,"props":1040,"children":1041},{"style":189},[1042],{"type":33,"value":628},{"type":24,"tag":160,"props":1044,"children":1045},{"style":173},[1046],{"type":33,"value":1047},"newVirtualThreadPerTaskExecutor",{"type":24,"tag":160,"props":1049,"children":1050},{"style":189},[1051],{"type":33,"value":1052},"()) {\n",{"type":24,"tag":160,"props":1054,"children":1055},{"class":162,"line":274},[1056,1061,1065,1069,1073,1077,1081,1085,1089,1094,1098,1102],{"type":24,"tag":160,"props":1057,"children":1058},{"style":167},[1059],{"type":33,"value":1060},"            for",{"type":24,"tag":160,"props":1062,"children":1063},{"style":189},[1064],{"type":33,"value":661},{"type":24,"tag":160,"props":1066,"children":1067},{"style":167},[1068],{"type":33,"value":666},{"type":24,"tag":160,"props":1070,"children":1071},{"style":189},[1072],{"type":33,"value":671},{"type":24,"tag":160,"props":1074,"children":1075},{"style":167},[1076],{"type":33,"value":383},{"type":24,"tag":160,"props":1078,"children":1079},{"style":640},[1080],{"type":33,"value":680},{"type":24,"tag":160,"props":1082,"children":1083},{"style":189},[1084],{"type":33,"value":685},{"type":24,"tag":160,"props":1086,"children":1087},{"style":167},[1088],{"type":33,"value":690},{"type":24,"tag":160,"props":1090,"children":1091},{"style":640},[1092],{"type":33,"value":1093}," 10",{"type":24,"tag":160,"props":1095,"children":1096},{"style":189},[1097],{"type":33,"value":700},{"type":24,"tag":160,"props":1099,"children":1100},{"style":167},[1101],{"type":33,"value":705},{"type":24,"tag":160,"props":1103,"children":1104},{"style":189},[1105],{"type":33,"value":369},{"type":24,"tag":160,"props":1107,"children":1108},{"class":162,"line":283},[1109,1114,1118,1122,1126],{"type":24,"tag":160,"props":1110,"children":1111},{"style":189},[1112],{"type":33,"value":1113},"                executor.",{"type":24,"tag":160,"props":1115,"children":1116},{"style":173},[1117],{"type":33,"value":722},{"type":24,"tag":160,"props":1119,"children":1120},{"style":189},[1121],{"type":33,"value":727},{"type":24,"tag":160,"props":1123,"children":1124},{"style":167},[1125],{"type":33,"value":732},{"type":24,"tag":160,"props":1127,"children":1128},{"style":189},[1129],{"type":33,"value":192},{"type":24,"tag":160,"props":1131,"children":1132},{"class":162,"line":292},[1133,1138,1142,1146,1151,1155,1159,1163,1167,1171],{"type":24,"tag":160,"props":1134,"children":1135},{"style":189},[1136],{"type":33,"value":1137},"                    System.out.",{"type":24,"tag":160,"props":1139,"children":1140},{"style":173},[1141],{"type":33,"value":230},{"type":24,"tag":160,"props":1143,"children":1144},{"style":189},[1145],{"type":33,"value":235},{"type":24,"tag":160,"props":1147,"children":1148},{"style":238},[1149],{"type":33,"value":1150},"\"Running in: \"",{"type":24,"tag":160,"props":1152,"children":1153},{"style":167},[1154],{"type":33,"value":246},{"type":24,"tag":160,"props":1156,"children":1157},{"style":189},[1158],{"type":33,"value":251},{"type":24,"tag":160,"props":1160,"children":1161},{"style":173},[1162],{"type":33,"value":256},{"type":24,"tag":160,"props":1164,"children":1165},{"style":189},[1166],{"type":33,"value":261},{"type":24,"tag":160,"props":1168,"children":1169},{"style":173},[1170],{"type":33,"value":266},{"type":24,"tag":160,"props":1172,"children":1173},{"style":189},[1174],{"type":33,"value":271},{"type":24,"tag":160,"props":1176,"children":1177},{"class":162,"line":302},[1178],{"type":24,"tag":160,"props":1179,"children":1180},{"style":189},[1181],{"type":33,"value":1182},"                });\n",{"type":24,"tag":160,"props":1184,"children":1185},{"class":162,"line":325},[1186],{"type":24,"tag":160,"props":1187,"children":1188},{"style":189},[1189],{"type":33,"value":1190},"            }\n",{"type":24,"tag":160,"props":1192,"children":1193},{"class":162,"line":372},[1194],{"type":24,"tag":160,"props":1195,"children":1196},{"style":189},[1197],{"type":33,"value":783},{"type":24,"tag":160,"props":1199,"children":1200},{"class":162,"line":400},[1201],{"type":24,"tag":160,"props":1202,"children":1203},{"style":189},[1204],{"type":33,"value":280},{"type":24,"tag":160,"props":1206,"children":1207},{"class":162,"line":418},[1208],{"type":24,"tag":160,"props":1209,"children":1210},{"style":189},[1211],{"type":33,"value":289},{"type":24,"tag":433,"props":1213,"children":1215},{"id":1214},"when-to-use-virtual-threads",[1216],{"type":33,"value":1217},"When to Use Virtual Threads:",{"type":24,"tag":48,"props":1219,"children":1220},{},[1221,1226,1231],{"type":24,"tag":52,"props":1222,"children":1223},{},[1224],{"type":33,"value":1225},"I/O-bound applications (e.g., database access, HTTP clients).",{"type":24,"tag":52,"props":1227,"children":1228},{},[1229],{"type":33,"value":1230},"High-concurrency workloads (e.g., messaging systems).",{"type":24,"tag":52,"props":1232,"children":1233},{},[1234],{"type":33,"value":1235},"When simplifying thread management is a priority.",{"type":24,"tag":36,"props":1237,"children":1239},{"id":1238},"conclusion",[1240],{"type":33,"value":1241},"Conclusion",{"type":24,"tag":29,"props":1243,"children":1244},{},[1245],{"type":33,"value":1246},"Java offers a flexible and powerful concurrency model. Classic Threads provide a foundational understanding, ExecutorService enables efficient thread reuse and task management, and Virtual Threads open the door to a new era of scalable concurrency with minimal overhead.",{"type":24,"tag":29,"props":1248,"children":1249},{},[1250],{"type":33,"value":1251},"Each model has its place depending on the problem at hand. Mastering them gives you the toolkit needed to build high-performance, concurrent applications.",{"type":24,"tag":36,"props":1253,"children":1255},{"id":1254},"further-reading",[1256],{"type":33,"value":1257},"Further Reading",{"type":24,"tag":48,"props":1259,"children":1260},{},[1261,1273,1283,1292],{"type":24,"tag":52,"props":1262,"children":1263},{},[1264],{"type":24,"tag":1265,"props":1266,"children":1270},"a",{"href":1267,"rel":1268},"https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html",[1269],"nofollow",[1271],{"type":33,"value":1272},"Java Concurrency API",{"type":24,"tag":52,"props":1274,"children":1275},{},[1276],{"type":24,"tag":1265,"props":1277,"children":1280},{"href":1278,"rel":1279},"https://openjdk.org/projects/loom/",[1269],[1281],{"type":33,"value":1282},"Project Loom (Virtual Threads)",{"type":24,"tag":52,"props":1284,"children":1285},{},[1286],{"type":24,"tag":1265,"props":1287,"children":1290},{"href":1288,"rel":1289},"https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html",[1269],[1291],{"type":33,"value":90},{"type":24,"tag":52,"props":1293,"children":1294},{},[1295],{"type":24,"tag":1265,"props":1296,"children":1299},{"href":1297,"rel":1298},"https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/",[1269],[1300],{"type":33,"value":1301},"Effective Java - Joshua Bloch",{"type":24,"tag":29,"props":1303,"children":1304},{},[1305],{"type":33,"value":1306},"Deep understanding of Java's concurrency tools can significantly improve your system’s performance, scalability, and maintainability.",{"type":24,"tag":1308,"props":1309,"children":1310},"style",{},[1311],{"type":33,"value":1312},"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":195,"depth":195,"links":1314},[1315,1316,1319,1324,1328,1329],{"id":38,"depth":195,"text":41},{"id":118,"depth":195,"text":121,"children":1317},[1318],{"id":435,"depth":219,"text":438},{"id":479,"depth":195,"text":482,"children":1320},[1321,1322,1323],{"id":505,"depth":219,"text":508},{"id":817,"depth":219,"text":820},{"id":870,"depth":219,"text":873},{"id":894,"depth":195,"text":897,"children":1325},[1326,1327],{"id":905,"depth":219,"text":908},{"id":1214,"depth":219,"text":1217},{"id":1238,"depth":195,"text":1241},{"id":1254,"depth":195,"text":1257},"markdown","content:posts:mastering-concurrency.md","content","posts/mastering-concurrency.md","posts/mastering-concurrency","md",[1337,1885,3043],{"_path":1338,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1339,"description":1340,"layout":11,"date":1341,"subtitle":1342,"image":1343,"optimized_image":1343,"category":14,"tags":1344,"author":19,"paginate":6,"body":1348,"_type":1330,"_id":1882,"_source":1332,"_file":1883,"_stem":1884,"_extension":1335},"/posts/chat-with-pdfs-locally-part-1","Part 1/3: Chat with Your PDFs Locally","Build a local PDF chat app that extracts text, indexes it in a vector database, and answers questions with document, section, and page references. Includes runnable code and a Makefile.","2025-12-15T11:00:00.000Z","Build a small RAG app with Ollama, Chroma, and source-aware answers","/assets/img/uploads/local-pdf-chat-part-one.jpg",[14,1345,1346,1347],"ai","python","architecture",{"type":21,"children":1349,"toc":1874},[1350,1361,1381,1387,1444,1475,1480,1493,1499,1533,1633,1685,1719,1725,1730,1774,1787,1792,1797,1803,1808,1813,1818,1824,1836,1841,1846,1852,1857,1870],{"type":24,"tag":29,"props":1351,"children":1352},{},[1353,1355],{"type":33,"value":1354},"I have plenty of PDFs that are easy to store and surprisingly hard to use. The answer to a question might be one paragraph inside a long guide, but finding it means remembering the right filename, opening it, and searching for the words the author happened to use. A chat box feels like a natural interface. The difficult part is making its answer traceable: ",{"type":24,"tag":1356,"props":1357,"children":1358},"em",{},[1359],{"type":33,"value":1360},"which document, which section, and which page support this claim?",{"type":24,"tag":29,"props":1362,"children":1363},{},[1364,1366,1379],{"type":33,"value":1365},"This is the first post in a three-part series. We will build a small, fully local app that accepts PDFs with selectable text, answers questions about them, and shows the source excerpts. ",{"type":24,"tag":1265,"props":1367,"children":1370},{"href":1368,"rel":1369},"https://github.com/JaimeDeArcos/document-manager",[1269],[1371,1373],{"type":33,"value":1372},"The complete Python project, README, and Makefile live in ",{"type":24,"tag":14,"props":1374,"children":1376},{"className":1375},[],[1377],{"type":33,"value":1378},"document-manager",{"type":33,"value":1380},". Part 2 will examine retrieval mistakes and how to evaluate answers; Part 3 will add OCR for scanned PDFs. The numbered titles are deliberate: each post extends the same working example.",{"type":24,"tag":36,"props":1382,"children":1384},{"id":1383},"what-we-are-building",[1385],{"type":33,"value":1386},"What we are building",{"type":24,"tag":29,"props":1388,"children":1389},{},[1390,1392,1399,1401,1408,1410,1417,1419,1426,1428,1434,1436,1442],{"type":33,"value":1391},"The interface is a ",{"type":24,"tag":1265,"props":1393,"children":1396},{"href":1394,"rel":1395},"https://docs.streamlit.io/develop/tutorials/chat-and-llm-apps/build-conversational-apps",[1269],[1397],{"type":33,"value":1398},"Streamlit",{"type":33,"value":1400}," chat. ",{"type":24,"tag":1265,"props":1402,"children":1405},{"href":1403,"rel":1404},"https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/",[1269],[1406],{"type":33,"value":1407},"PyMuPDF4LLM",{"type":33,"value":1409}," extracts text by page. ",{"type":24,"tag":1265,"props":1411,"children":1414},{"href":1412,"rel":1413},"https://docs.trychroma.com/reference/python",[1269],[1415],{"type":33,"value":1416},"Chroma",{"type":33,"value":1418}," stores text chunks, their metadata, and vectors on disk. ",{"type":24,"tag":1265,"props":1420,"children":1423},{"href":1421,"rel":1422},"https://ollama.com/download",[1269],[1424],{"type":33,"value":1425},"Ollama",{"type":33,"value":1427}," runs two local models: ",{"type":24,"tag":14,"props":1429,"children":1431},{"className":1430},[],[1432],{"type":33,"value":1433},"embeddinggemma",{"type":33,"value":1435}," to turn text into vectors and ",{"type":24,"tag":14,"props":1437,"children":1439},{"className":1438},[],[1440],{"type":33,"value":1441},"gemma3:4b",{"type":33,"value":1443}," to compose an answer from the retrieved passages.",{"type":24,"tag":151,"props":1445,"children":1448},{"className":1446,"code":1447,"language":33,"meta":7,"style":7},"language-text shiki shiki-themes github-dark github-light","PDF → extract text → split into chunks → embed → Chroma\n\nquestion → embed → retrieve nearby chunks → answer → show sources\n",[1449],{"type":24,"tag":14,"props":1450,"children":1451},{"__ignoreMap":7},[1452,1460,1467],{"type":24,"tag":160,"props":1453,"children":1454},{"class":162,"line":163},[1455],{"type":24,"tag":160,"props":1456,"children":1457},{},[1458],{"type":33,"value":1459},"PDF → extract text → split into chunks → embed → Chroma\n",{"type":24,"tag":160,"props":1461,"children":1462},{"class":162,"line":195},[1463],{"type":24,"tag":160,"props":1464,"children":1465},{"emptyLinePlaceholder":296},[1466],{"type":33,"value":299},{"type":24,"tag":160,"props":1468,"children":1469},{"class":162,"line":219},[1470],{"type":24,"tag":160,"props":1471,"children":1472},{},[1473],{"type":33,"value":1474},"question → embed → retrieve nearby chunks → answer → show sources\n",{"type":24,"tag":29,"props":1476,"children":1477},{},[1478],{"type":33,"value":1479},"This is retrieval-augmented generation, or RAG. The language model does not read every PDF on each question. We first select likely passages and supply only those passages as context. That makes the system more useful, but a vector search result is merely a candidate. It may be the closest passage while still being irrelevant. That distinction matters whenever the answer is uncertain.",{"type":24,"tag":29,"props":1481,"children":1482},{},[1483,1485,1491],{"type":33,"value":1484},"The app runs for one user on one machine. Uploaded PDFs and the vector index stay in its ",{"type":24,"tag":14,"props":1486,"children":1488},{"className":1487},[],[1489],{"type":33,"value":1490},"data/",{"type":33,"value":1492}," directory; the browser app binds to localhost. Installation and model downloads need internet access, but ordinary questions use the local Ollama service. There is no cloud API key or separate vector database server.",{"type":24,"tag":36,"props":1494,"children":1496},{"id":1495},"run-the-example",[1497],{"type":33,"value":1498},"Run the example",{"type":24,"tag":29,"props":1500,"children":1501},{},[1502,1504,1515,1517,1523,1525,1531],{"type":33,"value":1503},"Install Python 3.11 or newer, ",{"type":24,"tag":1265,"props":1505,"children":1508},{"href":1506,"rel":1507},"https://docs.astral.sh/uv/getting-started/installation/",[1269],[1509],{"type":24,"tag":14,"props":1510,"children":1512},{"className":1511},[],[1513],{"type":33,"value":1514},"uv",{"type":33,"value":1516},", Ollama, and ",{"type":24,"tag":14,"props":1518,"children":1520},{"className":1519},[],[1521],{"type":33,"value":1522},"make",{"type":33,"value":1524},". Start the Ollama app, or run ",{"type":24,"tag":14,"props":1526,"children":1528},{"className":1527},[],[1529],{"type":33,"value":1530},"ollama serve",{"type":33,"value":1532}," in a separate terminal. Then:",{"type":24,"tag":151,"props":1534,"children":1538},{"className":1535,"code":1536,"language":1537,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark github-light","git clone https://github.com/JaimeDeArcos/document-manager.git\ncd document-manager\nmake setup\nmake models\nmake doctor\nmake demo-pdf\nmake run\n","bash",[1539],{"type":24,"tag":14,"props":1540,"children":1541},{"__ignoreMap":7},[1542,1560,1573,1585,1597,1609,1621],{"type":24,"tag":160,"props":1543,"children":1544},{"class":162,"line":163},[1545,1550,1555],{"type":24,"tag":160,"props":1546,"children":1547},{"style":173},[1548],{"type":33,"value":1549},"git",{"type":24,"tag":160,"props":1551,"children":1552},{"style":238},[1553],{"type":33,"value":1554}," clone",{"type":24,"tag":160,"props":1556,"children":1557},{"style":238},[1558],{"type":33,"value":1559}," https://github.com/JaimeDeArcos/document-manager.git\n",{"type":24,"tag":160,"props":1561,"children":1562},{"class":162,"line":195},[1563,1568],{"type":24,"tag":160,"props":1564,"children":1565},{"style":640},[1566],{"type":33,"value":1567},"cd",{"type":24,"tag":160,"props":1569,"children":1570},{"style":238},[1571],{"type":33,"value":1572}," document-manager\n",{"type":24,"tag":160,"props":1574,"children":1575},{"class":162,"line":219},[1576,1580],{"type":24,"tag":160,"props":1577,"children":1578},{"style":173},[1579],{"type":33,"value":1522},{"type":24,"tag":160,"props":1581,"children":1582},{"style":238},[1583],{"type":33,"value":1584}," setup\n",{"type":24,"tag":160,"props":1586,"children":1587},{"class":162,"line":274},[1588,1592],{"type":24,"tag":160,"props":1589,"children":1590},{"style":173},[1591],{"type":33,"value":1522},{"type":24,"tag":160,"props":1593,"children":1594},{"style":238},[1595],{"type":33,"value":1596}," models\n",{"type":24,"tag":160,"props":1598,"children":1599},{"class":162,"line":283},[1600,1604],{"type":24,"tag":160,"props":1601,"children":1602},{"style":173},[1603],{"type":33,"value":1522},{"type":24,"tag":160,"props":1605,"children":1606},{"style":238},[1607],{"type":33,"value":1608}," doctor\n",{"type":24,"tag":160,"props":1610,"children":1611},{"class":162,"line":292},[1612,1616],{"type":24,"tag":160,"props":1613,"children":1614},{"style":173},[1615],{"type":33,"value":1522},{"type":24,"tag":160,"props":1617,"children":1618},{"style":238},[1619],{"type":33,"value":1620}," demo-pdf\n",{"type":24,"tag":160,"props":1622,"children":1623},{"class":162,"line":302},[1624,1628],{"type":24,"tag":160,"props":1625,"children":1626},{"style":173},[1627],{"type":33,"value":1522},{"type":24,"tag":160,"props":1629,"children":1630},{"style":238},[1631],{"type":33,"value":1632}," run\n",{"type":24,"tag":29,"props":1634,"children":1635},{},[1636,1642,1644,1650,1652,1658,1660,1666,1668,1674,1676,1683],{"type":24,"tag":14,"props":1637,"children":1639},{"className":1638},[],[1640],{"type":33,"value":1641},"make setup",{"type":33,"value":1643}," installs the locked Python dependencies. ",{"type":24,"tag":14,"props":1645,"children":1647},{"className":1646},[],[1648],{"type":33,"value":1649},"make models",{"type":33,"value":1651}," pulls both models into Ollama; this is the largest download. ",{"type":24,"tag":14,"props":1653,"children":1655},{"className":1654},[],[1656],{"type":33,"value":1657},"make doctor",{"type":33,"value":1659}," checks that the service and models are available. ",{"type":24,"tag":14,"props":1661,"children":1663},{"className":1662},[],[1664],{"type":33,"value":1665},"make run",{"type":33,"value":1667}," opens the app at ",{"type":24,"tag":14,"props":1669,"children":1671},{"className":1670},[],[1672],{"type":33,"value":1673},"http://127.0.0.1:8501",{"type":33,"value":1675},". The ",{"type":24,"tag":1265,"props":1677,"children":1680},{"href":1678,"rel":1679},"https://github.com/JaimeDeArcos/document-manager#readme",[1269],[1681],{"type":33,"value":1682},"README",{"type":33,"value":1684}," documents ports, environment variables, and troubleshooting.",{"type":24,"tag":29,"props":1686,"children":1687},{},[1688,1690,1696,1698,1703,1705,1710,1712,1717],{"type":33,"value":1689},"Upload ",{"type":24,"tag":14,"props":1691,"children":1693},{"className":1692},[],[1694],{"type":33,"value":1695},"data/demo-handbook.pdf",{"type":33,"value":1697}," in the sidebar and ask: ",{"type":24,"tag":56,"props":1699,"children":1700},{},[1701],{"type":33,"value":1702},"How long are project notes kept?",{"type":33,"value":1704}," The sample PDF says 90 days in its ",{"type":24,"tag":1356,"props":1706,"children":1707},{},[1708],{"type":33,"value":1709},"Retention",{"type":33,"value":1711}," section on page 1. The response should show that reference and an excerpt. Then ask ",{"type":24,"tag":56,"props":1713,"children":1714},{},[1715],{"type":33,"value":1716},"What is the holiday policy?",{"type":33,"value":1718}," There is no supported answer in that document, so the intended behavior is to say so. A local model can still make a mistake; check any cited excerpt rather than assuming this guardrail is infallible. You can also upload your own text-based PDFs, up to 25 MB each.",{"type":24,"tag":36,"props":1720,"children":1722},{"id":1721},"preserve-a-path-back-to-the-original-page",[1723],{"type":33,"value":1724},"Preserve a path back to the original page",{"type":24,"tag":29,"props":1726,"children":1727},{},[1728],{"type":33,"value":1729},"To cite a PDF, we need to retain its structure while indexing it. The extraction step requests page chunks from PyMuPDF4LLM:",{"type":24,"tag":151,"props":1731,"children":1734},{"className":1732,"code":1733,"language":1346,"meta":7,"style":7},"language-python shiki shiki-themes github-dark github-light","pages = pymupdf4llm.to_markdown(document, page_chunks=True)\n",[1735],{"type":24,"tag":14,"props":1736,"children":1737},{"__ignoreMap":7},[1738],{"type":24,"tag":160,"props":1739,"children":1740},{"class":162,"line":163},[1741,1746,1750,1755,1760,1764,1769],{"type":24,"tag":160,"props":1742,"children":1743},{"style":189},[1744],{"type":33,"value":1745},"pages ",{"type":24,"tag":160,"props":1747,"children":1748},{"style":167},[1749],{"type":33,"value":383},{"type":24,"tag":160,"props":1751,"children":1752},{"style":189},[1753],{"type":33,"value":1754}," pymupdf4llm.to_markdown(document, ",{"type":24,"tag":160,"props":1756,"children":1757},{"style":361},[1758],{"type":33,"value":1759},"page_chunks",{"type":24,"tag":160,"props":1761,"children":1762},{"style":167},[1763],{"type":33,"value":383},{"type":24,"tag":160,"props":1765,"children":1766},{"style":640},[1767],{"type":33,"value":1768},"True",{"type":24,"tag":160,"props":1770,"children":1771},{"style":189},[1772],{"type":33,"value":1773},")\n",{"type":24,"tag":29,"props":1775,"children":1776},{},[1777,1779,1785],{"type":33,"value":1778},"For each page, the app splits text into manageable chunks and stores the filename, one-based page number, and section label alongside the vector. A section label comes from a PDF table of contents entry or a Markdown heading detected in the extracted text. If neither exists, the honest label is ",{"type":24,"tag":14,"props":1780,"children":1782},{"className":1781},[],[1783],{"type":33,"value":1784},"Page N",{"type":33,"value":1786},". We do not ask the answer model to guess where a passage came from.",{"type":24,"tag":29,"props":1788,"children":1789},{},[1790],{"type":33,"value":1791},"Chunking has a trade-off. An entire long PDF is too broad for useful retrieval and too expensive to pass to the answer model. Tiny fragments lose context. This example caps chunks around 1,200 characters with a small overlap and keeps each chunk within its original page. It is a starting point, not a universal optimum. In Part 2 we will test whether those boundaries actually retrieve the evidence we need.",{"type":24,"tag":29,"props":1793,"children":1794},{},[1795],{"type":33,"value":1796},"The app rejects image-only scans with a clear OCR hint. Silently indexing an empty page would make later answers look confident while leaving the document effectively invisible.",{"type":24,"tag":36,"props":1798,"children":1800},{"id":1799},"index-and-replace-documents-without-stale-answers",[1801],{"type":33,"value":1802},"Index and replace documents without stale answers",{"type":24,"tag":29,"props":1804,"children":1805},{},[1806],{"type":33,"value":1807},"The embedding model converts each chunk to a vector. Chroma stores that vector together with the chunk text and source metadata. At question time, the same embedding model converts the question, and Chroma returns the nearest stored chunks. Using a different embedding model for queries and indexed documents would compare positions in different spaces; if you change models, rebuild the index.",{"type":24,"tag":29,"props":1809,"children":1810},{},[1811],{"type":33,"value":1812},"The index also tracks a hash of each PDF. Re-uploading the same bytes does nothing. Re-uploading changed bytes under the same filename replaces its old chunks, so a later question does not cite an outdated version of that document. The app adds the new vectors before removing the old ones, which protects the existing index if the new embedding or upload fails. You can remove a document from the sidebar as well.",{"type":24,"tag":29,"props":1814,"children":1815},{},[1816],{"type":33,"value":1817},"This replacement behavior is easy to overlook in a demo and important in a useful tool. An answer with a precise page number is not trustworthy if that page belongs to a file you replaced yesterday.",{"type":24,"tag":36,"props":1819,"children":1821},{"id":1820},"make-the-answer-inspectable",[1822],{"type":33,"value":1823},"Make the answer inspectable",{"type":24,"tag":29,"props":1825,"children":1826},{},[1827,1829,1834],{"type":33,"value":1828},"The app sends the answer model numbered excerpts, each prefixed with filename, section, and page. It asks for a JSON response with an answer and the source numbers used. The application accepts only citation numbers that correspond to retrieved excerpts, then displays those excerpts beneath the answer. This prevents a fabricated citation to a nonexistent result, although it does ",{"type":24,"tag":56,"props":1830,"children":1831},{},[1832],{"type":33,"value":1833},"not",{"type":33,"value":1835}," prove that the model interpreted a real excerpt correctly.",{"type":24,"tag":29,"props":1837,"children":1838},{},[1839],{"type":33,"value":1840},"If the model reports that the excerpts do not support an answer, or provides no valid source number, the app returns: “I couldn't find a supported answer in the indexed documents.” That is a useful outcome. The system should not turn the nearest passage into a fact simply because every search returns something.",{"type":24,"tag":29,"props":1842,"children":1843},{},[1844],{"type":33,"value":1845},"The source card is the final human check: read the cited paragraph, open the original PDF if needed, and verify the claim. The model is a shortcut to evidence, not a replacement for it. For decisions with real consequences, that distinction is part of the product design.",{"type":24,"tag":36,"props":1847,"children":1849},{"id":1848},"where-this-first-version-stops",[1850],{"type":33,"value":1851},"Where this first version stops",{"type":24,"tag":29,"props":1853,"children":1854},{},[1855],{"type":33,"value":1856},"This example intentionally has a small boundary. It handles selectable text, a single local user, and a straightforward nearest-neighbor search over chunks. It does not perform OCR, manage access permissions, or prove answer quality. Tables, multi-column layouts, and vague questions can still produce poor retrieval. Even a valid source reference can accompany an incorrect paraphrase.",{"type":24,"tag":29,"props":1858,"children":1859},{},[1860,1862,1868],{"type":33,"value":1861},"The next post will use this same project to ask a harder practical question: how do we know whether the right passage was retrieved and the answer is actually supported? For now, you have a working local application, a reproducible ",{"type":24,"tag":14,"props":1863,"children":1865},{"className":1864},[],[1866],{"type":33,"value":1867},"Makefile",{"type":33,"value":1869},", and source metadata that lets you check what it tells you.",{"type":24,"tag":1308,"props":1871,"children":1872},{},[1873],{"type":33,"value":1312},{"title":7,"searchDepth":195,"depth":195,"links":1875},[1876,1877,1878,1879,1880,1881],{"id":1383,"depth":195,"text":1386},{"id":1495,"depth":195,"text":1498},{"id":1721,"depth":195,"text":1724},{"id":1799,"depth":195,"text":1802},{"id":1820,"depth":195,"text":1823},{"id":1848,"depth":195,"text":1851},"content:posts:chat-with-pdfs-locally-part-1.md","posts/chat-with-pdfs-locally-part-1.md","posts/chat-with-pdfs-locally-part-1",{"_path":1886,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1887,"description":1888,"layout":11,"date":1889,"subtitle":1890,"image":1891,"optimized_image":1891,"category":14,"tags":1892,"author":19,"paginate":6,"body":1894,"_type":1330,"_id":3040,"_source":1332,"_file":3041,"_stem":3042,"_extension":1335},"/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.","2025-11-15T11: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,1345,1347,1893],"best practices",{"type":21,"children":1895,"toc":3033},[1896,1908,1939,2063,2084,2090,2103,2153,2165,2194,2257,2270,2276,2281,2295,2345,2375,2388,2402,2407,2446,2472,2557,2569,2575,2589,2602,2703,2733,2738,2744,2764,2775,2908,2938,2950,2956,2961,3019,3024,3029],{"type":24,"tag":29,"props":1897,"children":1898},{},[1899,1901,1906],{"type":33,"value":1900},"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":1356,"props":1902,"children":1903},{},[1904],{"type":33,"value":1905},"your",{"type":33,"value":1907}," repository, the behavior you promised users, or the checks your team expects before a change is done.",{"type":24,"tag":29,"props":1909,"children":1910},{},[1911,1913,1920,1922,1928,1930,1937],{"type":33,"value":1912},"My starting point is a small development workspace with five distinct parts: ",{"type":24,"tag":1265,"props":1914,"children":1917},{"href":1915,"rel":1916},"https://opencode.ai/docs/",[1269],[1918],{"type":33,"value":1919},"OpenCode",{"type":33,"value":1921}," to work in the repository, ",{"type":24,"tag":14,"props":1923,"children":1925},{"className":1924},[],[1926],{"type":33,"value":1927},"AGENTS.md",{"type":33,"value":1929}," to explain the repository, ",{"type":24,"tag":1265,"props":1931,"children":1934},{"href":1932,"rel":1933},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/getting-started.md",[1269],[1935],{"type":33,"value":1936},"OpenSpec",{"type":33,"value":1938}," 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":1940,"props":1941,"children":1942},"table",{},[1943,1967],{"type":24,"tag":1944,"props":1945,"children":1946},"thead",{},[1947],{"type":24,"tag":1948,"props":1949,"children":1950},"tr",{},[1951,1957,1962],{"type":24,"tag":1952,"props":1953,"children":1954},"th",{},[1955],{"type":33,"value":1956},"Piece",{"type":24,"tag":1952,"props":1958,"children":1959},{},[1960],{"type":33,"value":1961},"Question it answers",{"type":24,"tag":1952,"props":1963,"children":1964},{},[1965],{"type":33,"value":1966},"Example",{"type":24,"tag":1968,"props":1969,"children":1970},"tbody",{},[1971,1989,2010,2027,2045],{"type":24,"tag":1948,"props":1972,"children":1973},{},[1974,1979,1984],{"type":24,"tag":1975,"props":1976,"children":1977},"td",{},[1978],{"type":33,"value":1919},{"type":24,"tag":1975,"props":1980,"children":1981},{},[1982],{"type":33,"value":1983},"Who can inspect and change this repository with me?",{"type":24,"tag":1975,"props":1985,"children":1986},{},[1987],{"type":33,"value":1988},"Explore the API, edit code, run tests",{"type":24,"tag":1948,"props":1990,"children":1991},{},[1992,2000,2005],{"type":24,"tag":1975,"props":1993,"children":1994},{},[1995],{"type":24,"tag":14,"props":1996,"children":1998},{"className":1997},[],[1999],{"type":33,"value":1927},{"type":24,"tag":1975,"props":2001,"children":2002},{},[2003],{"type":33,"value":2004},"What should any agent know about this project?",{"type":24,"tag":1975,"props":2006,"children":2007},{},[2008],{"type":33,"value":2009},"Architecture, commands, conventions",{"type":24,"tag":1948,"props":2011,"children":2012},{},[2013,2017,2022],{"type":24,"tag":1975,"props":2014,"children":2015},{},[2016],{"type":33,"value":1936},{"type":24,"tag":1975,"props":2018,"children":2019},{},[2020],{"type":33,"value":2021},"What behavior are we agreeing to change?",{"type":24,"tag":1975,"props":2023,"children":2024},{},[2025],{"type":33,"value":2026},"A named change with scenarios and tasks",{"type":24,"tag":1948,"props":2028,"children":2029},{},[2030,2035,2040],{"type":24,"tag":1975,"props":2031,"children":2032},{},[2033],{"type":33,"value":2034},"Custom agent",{"type":24,"tag":1975,"props":2036,"children":2037},{},[2038],{"type":33,"value":2039},"Who should take a specialized role?",{"type":24,"tag":1975,"props":2041,"children":2042},{},[2043],{"type":33,"value":2044},"A reviewer with no edit permission",{"type":24,"tag":1948,"props":2046,"children":2047},{},[2048,2053,2058],{"type":24,"tag":1975,"props":2049,"children":2050},{},[2051],{"type":33,"value":2052},"Skill",{"type":24,"tag":1975,"props":2054,"children":2055},{},[2056],{"type":33,"value":2057},"How do we repeat a particular procedure?",{"type":24,"tag":1975,"props":2059,"children":2060},{},[2061],{"type":33,"value":2062},"A checklist for reviewing API compatibility",{"type":24,"tag":29,"props":2064,"children":2065},{},[2066,2068,2074,2076,2082],{"type":33,"value":2067},"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":2069,"children":2071},{"className":2070},[],[2072],{"type":33,"value":2073},"status",{"type":33,"value":2075}," filter on ",{"type":24,"tag":14,"props":2077,"children":2079},{"className":2078},[],[2080],{"type":33,"value":2081},"GET /tasks",{"type":33,"value":2083},".",{"type":24,"tag":36,"props":2085,"children":2087},{"id":2086},"start-with-repository-context",[2088],{"type":33,"value":2089},"Start with repository context",{"type":24,"tag":29,"props":2091,"children":2092},{},[2093,2095,2101],{"type":33,"value":2094},"Install OpenCode using one of the methods in its ",{"type":24,"tag":1265,"props":2096,"children":2098},{"href":1915,"rel":2097},[1269],[2099],{"type":33,"value":2100},"official setup guide",{"type":33,"value":2102},". For a Node-based setup, the documented command is:",{"type":24,"tag":151,"props":2104,"children":2106},{"className":1535,"code":2105,"language":1537,"meta":7,"style":7},"npm install -g opencode-ai\ncd path/to/your-project\nopencode\n",[2107],{"type":24,"tag":14,"props":2108,"children":2109},{"__ignoreMap":7},[2110,2133,2145],{"type":24,"tag":160,"props":2111,"children":2112},{"class":162,"line":163},[2113,2118,2123,2128],{"type":24,"tag":160,"props":2114,"children":2115},{"style":173},[2116],{"type":33,"value":2117},"npm",{"type":24,"tag":160,"props":2119,"children":2120},{"style":238},[2121],{"type":33,"value":2122}," install",{"type":24,"tag":160,"props":2124,"children":2125},{"style":640},[2126],{"type":33,"value":2127}," -g",{"type":24,"tag":160,"props":2129,"children":2130},{"style":238},[2131],{"type":33,"value":2132}," opencode-ai\n",{"type":24,"tag":160,"props":2134,"children":2135},{"class":162,"line":195},[2136,2140],{"type":24,"tag":160,"props":2137,"children":2138},{"style":640},[2139],{"type":33,"value":1567},{"type":24,"tag":160,"props":2141,"children":2142},{"style":238},[2143],{"type":33,"value":2144}," path/to/your-project\n",{"type":24,"tag":160,"props":2146,"children":2147},{"class":162,"line":219},[2148],{"type":24,"tag":160,"props":2149,"children":2150},{"style":173},[2151],{"type":33,"value":2152},"opencode\n",{"type":24,"tag":29,"props":2154,"children":2155},{},[2156,2158,2163],{"type":33,"value":2157},"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":2159,"children":2161},{"className":2160},[],[2162],{"type":33,"value":2081},{"type":33,"value":2164}," 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":29,"props":2166,"children":2167},{},[2168,2170,2175,2177,2183,2185,2192],{"type":33,"value":2169},"OpenCode can create a starter ",{"type":24,"tag":14,"props":2171,"children":2173},{"className":2172},[],[2174],{"type":33,"value":1927},{"type":33,"value":2176}," with ",{"type":24,"tag":14,"props":2178,"children":2180},{"className":2179},[],[2181],{"type":33,"value":2182},"/init",{"type":33,"value":2184},". 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":1265,"props":2186,"children":2189},{"href":2187,"rel":2188},"https://opencode.ai/docs/rules/",[1269],[2190],{"type":33,"value":2191},"rules documentation",{"type":33,"value":2193}," recommends project-specific instructions such as build commands, architecture, and conventions. A compact example:",{"type":24,"tag":151,"props":2195,"children":2198},{"className":2196,"code":2197,"language":1330,"meta":7,"style":7},"language-markdown shiki shiki-themes github-dark github-light","# 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",[2199],{"type":24,"tag":14,"props":2200,"children":2201},{"__ignoreMap":7},[2202,2210,2217,2225,2233,2241,2249],{"type":24,"tag":160,"props":2203,"children":2204},{"class":162,"line":163},[2205],{"type":24,"tag":160,"props":2206,"children":2207},{},[2208],{"type":33,"value":2209},"# Task API\n",{"type":24,"tag":160,"props":2211,"children":2212},{"class":162,"line":195},[2213],{"type":24,"tag":160,"props":2214,"children":2215},{"emptyLinePlaceholder":296},[2216],{"type":33,"value":299},{"type":24,"tag":160,"props":2218,"children":2219},{"class":162,"line":219},[2220],{"type":24,"tag":160,"props":2221,"children":2222},{},[2223],{"type":33,"value":2224},"- Routes live in `src/http`; persistence lives in `src/repositories`.\n",{"type":24,"tag":160,"props":2226,"children":2227},{"class":162,"line":274},[2228],{"type":24,"tag":160,"props":2229,"children":2230},{},[2231],{"type":33,"value":2232},"- Preserve the response shape of existing endpoints.\n",{"type":24,"tag":160,"props":2234,"children":2235},{"class":162,"line":283},[2236],{"type":24,"tag":160,"props":2237,"children":2238},{},[2239],{"type":33,"value":2240},"- Run `npm test` for behavior changes and `npm run build` before handoff.\n",{"type":24,"tag":160,"props":2242,"children":2243},{"class":162,"line":292},[2244],{"type":24,"tag":160,"props":2245,"children":2246},{},[2247],{"type":33,"value":2248},"- Do not modify database migrations that have already been applied.\n",{"type":24,"tag":160,"props":2250,"children":2251},{"class":162,"line":302},[2252],{"type":24,"tag":160,"props":2253,"children":2254},{},[2255],{"type":33,"value":2256},"- Record API contract changes in `openapi.yaml`.\n",{"type":24,"tag":29,"props":2258,"children":2259},{},[2260,2262,2268],{"type":33,"value":2261},"The exact paths and commands must match your repository. If your actual check is ",{"type":24,"tag":14,"props":2263,"children":2265},{"className":2264},[],[2266],{"type":33,"value":2267},"./gradlew test",{"type":33,"value":2269},", 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":36,"props":2271,"children":2273},{"id":2272},"give-the-change-a-contract-with-openspec",[2274],{"type":33,"value":2275},"Give the change a contract with OpenSpec",{"type":24,"tag":29,"props":2277,"children":2278},{},[2279],{"type":33,"value":2280},"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":29,"props":2282,"children":2283},{},[2284,2286,2293],{"type":33,"value":2285},"OpenSpec can be installed and initialized from the terminal. Its ",{"type":24,"tag":1265,"props":2287,"children":2290},{"href":2288,"rel":2289},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md",[1269],[2291],{"type":33,"value":2292},"installation guide",{"type":33,"value":2294}," documents the Node requirement and setup commands:",{"type":24,"tag":151,"props":2296,"children":2298},{"className":1535,"code":2297,"language":1537,"meta":7,"style":7},"npm install -g @fission-ai/openspec@latest\nopenspec init --tools opencode\n",[2299],{"type":24,"tag":14,"props":2300,"children":2301},{"__ignoreMap":7},[2302,2322],{"type":24,"tag":160,"props":2303,"children":2304},{"class":162,"line":163},[2305,2309,2313,2317],{"type":24,"tag":160,"props":2306,"children":2307},{"style":173},[2308],{"type":33,"value":2117},{"type":24,"tag":160,"props":2310,"children":2311},{"style":238},[2312],{"type":33,"value":2122},{"type":24,"tag":160,"props":2314,"children":2315},{"style":640},[2316],{"type":33,"value":2127},{"type":24,"tag":160,"props":2318,"children":2319},{"style":238},[2320],{"type":33,"value":2321}," @fission-ai/openspec@latest\n",{"type":24,"tag":160,"props":2323,"children":2324},{"class":162,"line":195},[2325,2330,2335,2340],{"type":24,"tag":160,"props":2326,"children":2327},{"style":173},[2328],{"type":33,"value":2329},"openspec",{"type":24,"tag":160,"props":2331,"children":2332},{"style":238},[2333],{"type":33,"value":2334}," init",{"type":24,"tag":160,"props":2336,"children":2337},{"style":640},[2338],{"type":33,"value":2339}," --tools",{"type":24,"tag":160,"props":2341,"children":2342},{"style":238},[2343],{"type":33,"value":2344}," opencode\n",{"type":24,"tag":29,"props":2346,"children":2347},{},[2348,2350,2356,2358,2364,2366,2373],{"type":33,"value":2349},"Run ",{"type":24,"tag":14,"props":2351,"children":2353},{"className":2352},[],[2354],{"type":33,"value":2355},"openspec init",{"type":33,"value":2357}," at the project root. The ",{"type":24,"tag":14,"props":2359,"children":2361},{"className":2360},[],[2362],{"type":33,"value":2363},"--tools opencode",{"type":33,"value":2365}," 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":1265,"props":2367,"children":2370},{"href":2368,"rel":2369},"https://github.com/Fission-AI/OpenSpec/blob/main/docs/supported-tools.md",[1269],[2371],{"type":33,"value":2372},"tool reference",{"type":33,"value":2374}," describes those differences.",{"type":24,"tag":29,"props":2376,"children":2377},{},[2378,2380,2386],{"type":33,"value":2379},"In OpenCode, the OpenSpec integration currently provides ",{"type":24,"tag":14,"props":2381,"children":2383},{"className":2382},[],[2384],{"type":33,"value":2385},"/opsx-propose",{"type":33,"value":2387}," for this step. Start a named change in the chat:",{"type":24,"tag":151,"props":2389,"children":2391},{"className":1446,"code":2390,"language":33,"meta":7,"style":7},"/opsx-propose add-task-status-filter\n",[2392],{"type":24,"tag":14,"props":2393,"children":2394},{"__ignoreMap":7},[2395],{"type":24,"tag":160,"props":2396,"children":2397},{"class":162,"line":163},[2398],{"type":24,"tag":160,"props":2399,"children":2400},{},[2401],{"type":33,"value":2390},{"type":24,"tag":29,"props":2403,"children":2404},{},[2405],{"type":33,"value":2406},"Give it the context the command needs:",{"type":24,"tag":151,"props":2408,"children":2410},{"className":1446,"code":2409,"language":33,"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",[2411],{"type":24,"tag":14,"props":2412,"children":2413},{"__ignoreMap":7},[2414,2422,2430,2438],{"type":24,"tag":160,"props":2415,"children":2416},{"class":162,"line":163},[2417],{"type":24,"tag":160,"props":2418,"children":2419},{},[2420],{"type":33,"value":2421},"Propose a change to add an optional status filter to GET /tasks.\n",{"type":24,"tag":160,"props":2423,"children":2424},{"class":162,"line":195},[2425],{"type":24,"tag":160,"props":2426,"children":2427},{},[2428],{"type":33,"value":2429},"Inspect the current endpoint and tests first. Define the behavior when the\n",{"type":24,"tag":160,"props":2431,"children":2432},{"class":162,"line":219},[2433],{"type":24,"tag":160,"props":2434,"children":2435},{},[2436],{"type":33,"value":2437},"parameter is absent, when it is valid, and when it is invalid. Do not\n",{"type":24,"tag":160,"props":2439,"children":2440},{"class":162,"line":274},[2441],{"type":24,"tag":160,"props":2442,"children":2443},{},[2444],{"type":33,"value":2445},"implement the change until we have reviewed the proposal and scenarios.\n",{"type":24,"tag":29,"props":2447,"children":2448},{},[2449,2451,2456,2458,2463,2465,2470],{"type":33,"value":2450},"The generated command is part of the OpenSpec workflow installed for OpenCode; check the names printed by ",{"type":24,"tag":14,"props":2452,"children":2454},{"className":2453},[],[2455],{"type":33,"value":2355},{"type":33,"value":2457}," if your installation differs. In the default spec-driven structure, a proposal explains ",{"type":24,"tag":1356,"props":2459,"children":2460},{},[2461],{"type":33,"value":2462},"why",{"type":33,"value":2464},", a spec describes ",{"type":24,"tag":1356,"props":2466,"children":2467},{},[2468],{"type":33,"value":2469},"observable behavior",{"type":33,"value":2471},", a design captures technical decisions when needed, and tasks track implementation. A scenario might say:",{"type":24,"tag":151,"props":2473,"children":2475},{"className":2196,"code":2474,"language":1330,"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",[2476],{"type":24,"tag":14,"props":2477,"children":2478},{"__ignoreMap":7},[2479,2487,2495,2502,2510,2518,2526,2533,2541,2549],{"type":24,"tag":160,"props":2480,"children":2481},{"class":162,"line":163},[2482],{"type":24,"tag":160,"props":2483,"children":2484},{},[2485],{"type":33,"value":2486},"### Requirement: Filter tasks by status\n",{"type":24,"tag":160,"props":2488,"children":2489},{"class":162,"line":195},[2490],{"type":24,"tag":160,"props":2491,"children":2492},{},[2493],{"type":33,"value":2494},"The API MUST allow clients to filter tasks by a supported status.\n",{"type":24,"tag":160,"props":2496,"children":2497},{"class":162,"line":219},[2498],{"type":24,"tag":160,"props":2499,"children":2500},{"emptyLinePlaceholder":296},[2501],{"type":33,"value":299},{"type":24,"tag":160,"props":2503,"children":2504},{"class":162,"line":274},[2505],{"type":24,"tag":160,"props":2506,"children":2507},{},[2508],{"type":33,"value":2509},"#### Scenario: No filter\n",{"type":24,"tag":160,"props":2511,"children":2512},{"class":162,"line":283},[2513],{"type":24,"tag":160,"props":2514,"children":2515},{},[2516],{"type":33,"value":2517},"- **WHEN** a client calls GET /tasks without a status parameter\n",{"type":24,"tag":160,"props":2519,"children":2520},{"class":162,"line":292},[2521],{"type":24,"tag":160,"props":2522,"children":2523},{},[2524],{"type":33,"value":2525},"- **THEN** the existing unfiltered behavior is preserved\n",{"type":24,"tag":160,"props":2527,"children":2528},{"class":162,"line":302},[2529],{"type":24,"tag":160,"props":2530,"children":2531},{"emptyLinePlaceholder":296},[2532],{"type":33,"value":299},{"type":24,"tag":160,"props":2534,"children":2535},{"class":162,"line":325},[2536],{"type":24,"tag":160,"props":2537,"children":2538},{},[2539],{"type":33,"value":2540},"#### Scenario: Unsupported status\n",{"type":24,"tag":160,"props":2542,"children":2543},{"class":162,"line":372},[2544],{"type":24,"tag":160,"props":2545,"children":2546},{},[2547],{"type":33,"value":2548},"- **WHEN** a client requests a status the API does not support\n",{"type":24,"tag":160,"props":2550,"children":2551},{"class":162,"line":400},[2552],{"type":24,"tag":160,"props":2553,"children":2554},{},[2555],{"type":33,"value":2556},"- **THEN** the API returns the documented validation error\n",{"type":24,"tag":29,"props":2558,"children":2559},{},[2560,2562,2568],{"type":33,"value":2561},"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":1265,"props":2563,"children":2565},{"href":2564},"/spec-driven-development-openspec/",[2566],{"type":33,"value":2567},"OpenSpec article",{"type":33,"value":2083},{"type":24,"tag":36,"props":2570,"children":2572},{"id":2571},"add-a-specialist-only-when-it-has-a-clear-job",[2573],{"type":33,"value":2574},"Add a specialist only when it has a clear job",{"type":24,"tag":29,"props":2576,"children":2577},{},[2578,2580,2587],{"type":33,"value":2579},"Once the change is implemented, a second perspective can help. OpenCode supports ",{"type":24,"tag":1265,"props":2581,"children":2584},{"href":2582,"rel":2583},"https://opencode.ai/docs/agents/",[1269],[2585],{"type":33,"value":2586},"custom primary agents and subagents",{"type":33,"value":2588},". 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":29,"props":2590,"children":2591},{},[2592,2594,2600],{"type":33,"value":2593},"Create ",{"type":24,"tag":14,"props":2595,"children":2597},{"className":2596},[],[2598],{"type":33,"value":2599},".opencode/agents/api-reviewer.md",{"type":33,"value":2601},":",{"type":24,"tag":151,"props":2603,"children":2605},{"className":2196,"code":2604,"language":1330,"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",[2606],{"type":24,"tag":14,"props":2607,"children":2608},{"__ignoreMap":7},[2609,2617,2625,2633,2641,2649,2657,2664,2671,2679,2687,2695],{"type":24,"tag":160,"props":2610,"children":2611},{"class":162,"line":163},[2612],{"type":24,"tag":160,"props":2613,"children":2614},{},[2615],{"type":33,"value":2616},"---\n",{"type":24,"tag":160,"props":2618,"children":2619},{"class":162,"line":195},[2620],{"type":24,"tag":160,"props":2621,"children":2622},{},[2623],{"type":33,"value":2624},"description: Reviews API changes against their contract and existing clients\n",{"type":24,"tag":160,"props":2626,"children":2627},{"class":162,"line":219},[2628],{"type":24,"tag":160,"props":2629,"children":2630},{},[2631],{"type":33,"value":2632},"mode: subagent\n",{"type":24,"tag":160,"props":2634,"children":2635},{"class":162,"line":274},[2636],{"type":24,"tag":160,"props":2637,"children":2638},{},[2639],{"type":33,"value":2640},"permission:\n",{"type":24,"tag":160,"props":2642,"children":2643},{"class":162,"line":283},[2644],{"type":24,"tag":160,"props":2645,"children":2646},{},[2647],{"type":33,"value":2648},"  edit: deny\n",{"type":24,"tag":160,"props":2650,"children":2651},{"class":162,"line":292},[2652],{"type":24,"tag":160,"props":2653,"children":2654},{},[2655],{"type":33,"value":2656},"  bash: ask\n",{"type":24,"tag":160,"props":2658,"children":2659},{"class":162,"line":302},[2660],{"type":24,"tag":160,"props":2661,"children":2662},{},[2663],{"type":33,"value":2616},{"type":24,"tag":160,"props":2665,"children":2666},{"class":162,"line":325},[2667],{"type":24,"tag":160,"props":2668,"children":2669},{"emptyLinePlaceholder":296},[2670],{"type":33,"value":299},{"type":24,"tag":160,"props":2672,"children":2673},{"class":162,"line":372},[2674],{"type":24,"tag":160,"props":2675,"children":2676},{},[2677],{"type":33,"value":2678},"Review the proposed API change and its tests. Compare the implementation\n",{"type":24,"tag":160,"props":2680,"children":2681},{"class":162,"line":400},[2682],{"type":24,"tag":160,"props":2683,"children":2684},{},[2685],{"type":33,"value":2686},"with the OpenSpec scenarios and the existing endpoint behavior. Look for\n",{"type":24,"tag":160,"props":2688,"children":2689},{"class":162,"line":418},[2690],{"type":24,"tag":160,"props":2691,"children":2692},{},[2693],{"type":33,"value":2694},"compatibility changes, pagination mistakes, invalid input handling, and\n",{"type":24,"tag":160,"props":2696,"children":2697},{"class":162,"line":426},[2698],{"type":24,"tag":160,"props":2699,"children":2700},{},[2701],{"type":33,"value":2702},"missing tests. Cite the files and lines behind each finding. Do not edit.\n",{"type":24,"tag":29,"props":2704,"children":2705},{},[2706,2708,2714,2716,2722,2724,2731],{"type":33,"value":2707},"You can invoke that subagent by mentioning ",{"type":24,"tag":14,"props":2709,"children":2711},{"className":2710},[],[2712],{"type":33,"value":2713},"@api-reviewer",{"type":33,"value":2715}," in OpenCode. It cannot edit files, and it must ask before running a shell command such as ",{"type":24,"tag":14,"props":2717,"children":2719},{"className":2718},[],[2720],{"type":33,"value":2721},"git diff",{"type":33,"value":2723},". 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":1265,"props":2725,"children":2728},{"href":2726,"rel":2727},"https://opencode.ai/docs/permissions/",[1269],[2729],{"type":33,"value":2730},"permissions guide",{"type":33,"value":2732}," is worth reading before giving a specialist shell or editing access.",{"type":24,"tag":29,"props":2734,"children":2735},{},[2736],{"type":33,"value":2737},"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":36,"props":2739,"children":2741},{"id":2740},"capture-a-procedure-as-a-skill",[2742],{"type":33,"value":2743},"Capture a procedure as a skill",{"type":24,"tag":29,"props":2745,"children":2746},{},[2747,2749,2754,2756,2762],{"type":33,"value":2748},"A skill is different from an agent. It packages instructions for ",{"type":24,"tag":1356,"props":2750,"children":2751},{},[2752],{"type":33,"value":2753},"how",{"type":33,"value":2755}," 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":2757,"children":2759},{"className":2758},[],[2760],{"type":33,"value":2761},"api-compatibility",{"type":33,"value":2763}," skill could define the checks the reviewer should apply across many API changes.",{"type":24,"tag":29,"props":2765,"children":2766},{},[2767,2768,2774],{"type":33,"value":2593},{"type":24,"tag":14,"props":2769,"children":2771},{"className":2770},[],[2772],{"type":33,"value":2773},".opencode/skills/api-compatibility/SKILL.md",{"type":33,"value":2601},{"type":24,"tag":151,"props":2776,"children":2778},{"className":2196,"code":2777,"language":1330,"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",[2779],{"type":24,"tag":14,"props":2780,"children":2781},{"__ignoreMap":7},[2782,2789,2797,2805,2812,2819,2827,2834,2842,2849,2857,2864,2872,2881,2890,2899],{"type":24,"tag":160,"props":2783,"children":2784},{"class":162,"line":163},[2785],{"type":24,"tag":160,"props":2786,"children":2787},{},[2788],{"type":33,"value":2616},{"type":24,"tag":160,"props":2790,"children":2791},{"class":162,"line":195},[2792],{"type":24,"tag":160,"props":2793,"children":2794},{},[2795],{"type":33,"value":2796},"name: api-compatibility\n",{"type":24,"tag":160,"props":2798,"children":2799},{"class":162,"line":219},[2800],{"type":24,"tag":160,"props":2801,"children":2802},{},[2803],{"type":33,"value":2804},"description: Check an API change for client-visible compatibility risks\n",{"type":24,"tag":160,"props":2806,"children":2807},{"class":162,"line":274},[2808],{"type":24,"tag":160,"props":2809,"children":2810},{},[2811],{"type":33,"value":2616},{"type":24,"tag":160,"props":2813,"children":2814},{"class":162,"line":283},[2815],{"type":24,"tag":160,"props":2816,"children":2817},{"emptyLinePlaceholder":296},[2818],{"type":33,"value":299},{"type":24,"tag":160,"props":2820,"children":2821},{"class":162,"line":292},[2822],{"type":24,"tag":160,"props":2823,"children":2824},{},[2825],{"type":33,"value":2826},"## Use this skill when\n",{"type":24,"tag":160,"props":2828,"children":2829},{"class":162,"line":302},[2830],{"type":24,"tag":160,"props":2831,"children":2832},{"emptyLinePlaceholder":296},[2833],{"type":33,"value":299},{"type":24,"tag":160,"props":2835,"children":2836},{"class":162,"line":325},[2837],{"type":24,"tag":160,"props":2838,"children":2839},{},[2840],{"type":33,"value":2841},"An endpoint, request, response, or documented error behavior changes.\n",{"type":24,"tag":160,"props":2843,"children":2844},{"class":162,"line":372},[2845],{"type":24,"tag":160,"props":2846,"children":2847},{"emptyLinePlaceholder":296},[2848],{"type":33,"value":299},{"type":24,"tag":160,"props":2850,"children":2851},{"class":162,"line":400},[2852],{"type":24,"tag":160,"props":2853,"children":2854},{},[2855],{"type":33,"value":2856},"## Procedure\n",{"type":24,"tag":160,"props":2858,"children":2859},{"class":162,"line":418},[2860],{"type":24,"tag":160,"props":2861,"children":2862},{"emptyLinePlaceholder":296},[2863],{"type":33,"value":299},{"type":24,"tag":160,"props":2865,"children":2866},{"class":162,"line":426},[2867],{"type":24,"tag":160,"props":2868,"children":2869},{},[2870],{"type":33,"value":2871},"1. Read the current API contract and at least one existing client call.\n",{"type":24,"tag":160,"props":2873,"children":2875},{"class":162,"line":2874},13,[2876],{"type":24,"tag":160,"props":2877,"children":2878},{},[2879],{"type":33,"value":2880},"2. Compare old and new request and response shapes.\n",{"type":24,"tag":160,"props":2882,"children":2884},{"class":162,"line":2883},14,[2885],{"type":24,"tag":160,"props":2886,"children":2887},{},[2888],{"type":33,"value":2889},"3. Check defaults, invalid input, pagination, and error status codes.\n",{"type":24,"tag":160,"props":2891,"children":2893},{"class":162,"line":2892},15,[2894],{"type":24,"tag":160,"props":2895,"children":2896},{},[2897],{"type":33,"value":2898},"4. Identify changes that require clients to deploy at the same time.\n",{"type":24,"tag":160,"props":2900,"children":2902},{"class":162,"line":2901},16,[2903],{"type":24,"tag":160,"props":2904,"children":2905},{},[2906],{"type":33,"value":2907},"5. Report each risk with evidence and a compatible migration option.\n",{"type":24,"tag":29,"props":2909,"children":2910},{},[2911,2913,2920,2922,2928,2930,2936],{"type":33,"value":2912},"OpenCode's ",{"type":24,"tag":1265,"props":2914,"children":2917},{"href":2915,"rel":2916},"https://opencode.ai/docs/skills/",[1269],[2918],{"type":33,"value":2919},"skill documentation",{"type":33,"value":2921}," describes this directory structure and the required ",{"type":24,"tag":14,"props":2923,"children":2925},{"className":2924},[],[2926],{"type":33,"value":2927},"name",{"type":33,"value":2929}," and ",{"type":24,"tag":14,"props":2931,"children":2933},{"className":2932},[],[2934],{"type":33,"value":2935},"description",{"type":33,"value":2937}," 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":29,"props":2939,"children":2940},{},[2941,2943,2948],{"type":33,"value":2942},"You can ask the reviewer to use ",{"type":24,"tag":14,"props":2944,"children":2946},{"className":2945},[],[2947],{"type":33,"value":2761},{"type":33,"value":2949},", 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":36,"props":2951,"children":2953},{"id":2952},"run-one-complete-change",[2954],{"type":33,"value":2955},"Run one complete change",{"type":24,"tag":29,"props":2957,"children":2958},{},[2959],{"type":33,"value":2960},"With those pieces in place, the loop is straightforward:",{"type":24,"tag":2962,"props":2963,"children":2964},"ol",{},[2965,2970,2975,2994,3006],{"type":24,"tag":52,"props":2966,"children":2967},{},[2968],{"type":33,"value":2969},"Ask OpenCode to inspect the current endpoint and tests.",{"type":24,"tag":52,"props":2971,"children":2972},{},[2973],{"type":33,"value":2974},"Use OpenSpec to propose the filter and review its scenarios. Resolve ambiguous client behavior before coding.",{"type":24,"tag":52,"props":2976,"children":2977},{},[2978,2980,2986,2988,2993],{"type":33,"value":2979},"Use ",{"type":24,"tag":14,"props":2981,"children":2983},{"className":2982},[],[2984],{"type":33,"value":2985},"/opsx-apply",{"type":33,"value":2987}," to implement the approved change, then run the repository checks from ",{"type":24,"tag":14,"props":2989,"children":2991},{"className":2990},[],[2992],{"type":33,"value":1927},{"type":33,"value":2083},{"type":24,"tag":52,"props":2995,"children":2996},{},[2997,2999,3004],{"type":33,"value":2998},"Ask ",{"type":24,"tag":14,"props":3000,"children":3002},{"className":3001},[],[3003],{"type":33,"value":2713},{"type":33,"value":3005}," to compare the result with the spec and apply the compatibility procedure.",{"type":24,"tag":52,"props":3007,"children":3008},{},[3009,3011,3017],{"type":33,"value":3010},"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":3012,"children":3014},{"className":3013},[],[3015],{"type":33,"value":3016},"/opsx-archive",{"type":33,"value":3018}," once its implementation is finished and accepted.",{"type":24,"tag":29,"props":3020,"children":3021},{},[3022],{"type":33,"value":3023},"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":29,"props":3025,"children":3026},{},[3027],{"type":33,"value":3028},"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":1308,"props":3030,"children":3031},{},[3032],{"type":33,"value":1312},{"title":7,"searchDepth":195,"depth":195,"links":3034},[3035,3036,3037,3038,3039],{"id":2086,"depth":195,"text":2089},{"id":2272,"depth":195,"text":2275},{"id":2571,"depth":195,"text":2574},{"id":2740,"depth":195,"text":2743},{"id":2952,"depth":195,"text":2955},"content:posts:ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec.md","posts/ai-development-workspace-opencode-openspec",{"_path":3044,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3045,"description":3046,"layout":11,"date":3047,"subtitle":3048,"image":3049,"optimized_image":3049,"category":14,"tags":3050,"author":19,"paginate":6,"body":3052,"_type":1330,"_id":4440,"_source":1332,"_file":4441,"_stem":4442,"_extension":1335},"/posts/spec-driven-development-openspec","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.","2025-08-15T11:00:00.000Z","Turn an idea into behavior you can review, implement, and test","/assets/img/uploads/sdd-openspec.jpg",[14,1347,1345,3051],"testing",{"type":21,"children":3053,"toc":4428},[3054,3059,3071,3076,3082,3087,3190,3209,3214,3220,3225,3293,3366,3371,3502,3507,3513,3526,3531,3774,3787,3808,3814,3819,3904,3917,3929,3935,3945,3984,3989,4003,4009,4014,4053,4080,4093,4099,4118,4123,4162,4174,4180,4185,4284,4289,4336,4341,4361,4367,4380,4385,4390,4424],{"type":24,"tag":29,"props":3055,"children":3056},{},[3057],{"type":33,"value":3058},"“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":29,"props":3060,"children":3061},{},[3062,3064,3069],{"type":33,"value":3063},"I use OpenSpec for this kind of conversation: make the intended behavior visible before implementation hides the unanswered questions. ",{"type":24,"tag":56,"props":3065,"children":3066},{},[3067],{"type":33,"value":3068},"Spec-driven development (SDD) is useful when the expensive mistake is building a plausible interpretation of an unclear request.",{"type":33,"value":3070}," It does not remove judgment from the engineer. It gives people and AI agents something concrete to review, challenge, and verify.",{"type":24,"tag":29,"props":3072,"children":3073},{},[3074],{"type":33,"value":3075},"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":36,"props":3077,"children":3079},{"id":3078},"the-request-is-not-yet-a-specification",[3080],{"type":33,"value":3081},"The request is not yet a specification",{"type":24,"tag":29,"props":3083,"children":3084},{},[3085],{"type":33,"value":3086},"Before opening an editor, ask what “cancel” means in this service:",{"type":24,"tag":1940,"props":3088,"children":3089},{},[3090,3106],{"type":24,"tag":1944,"props":3091,"children":3092},{},[3093],{"type":24,"tag":1948,"props":3094,"children":3095},{},[3096,3101],{"type":24,"tag":1952,"props":3097,"children":3098},{},[3099],{"type":33,"value":3100},"Question",{"type":24,"tag":1952,"props":3102,"children":3103},{},[3104],{"type":33,"value":3105},"A decision the team needs to make",{"type":24,"tag":1968,"props":3107,"children":3108},{},[3109,3122,3143,3156,3169],{"type":24,"tag":1948,"props":3110,"children":3111},{},[3112,3117],{"type":24,"tag":1975,"props":3113,"children":3114},{},[3115],{"type":33,"value":3116},"Who may cancel?",{"type":24,"tag":1975,"props":3118,"children":3119},{},[3120],{"type":33,"value":3121},"The order owner, an administrator, or both?",{"type":24,"tag":1948,"props":3123,"children":3124},{},[3125,3130],{"type":24,"tag":1975,"props":3126,"children":3127},{},[3128],{"type":33,"value":3129},"Until when?",{"type":24,"tag":1975,"props":3131,"children":3132},{},[3133,3135,3141],{"type":33,"value":3134},"Only while ",{"type":24,"tag":14,"props":3136,"children":3138},{"className":3137},[],[3139],{"type":33,"value":3140},"PENDING",{"type":33,"value":3142},", or also after payment and before shipment?",{"type":24,"tag":1948,"props":3144,"children":3145},{},[3146,3151],{"type":24,"tag":1975,"props":3147,"children":3148},{},[3149],{"type":33,"value":3150},"What does a retry do?",{"type":24,"tag":1975,"props":3152,"children":3153},{},[3154],{"type":33,"value":3155},"Return the existing cancellation, or fail?",{"type":24,"tag":1948,"props":3157,"children":3158},{},[3159,3164],{"type":24,"tag":1975,"props":3160,"children":3161},{},[3162],{"type":33,"value":3163},"What if two requests race?",{"type":24,"tag":1975,"props":3165,"children":3166},{},[3167],{"type":33,"value":3168},"Can both succeed without recording two transitions?",{"type":24,"tag":1948,"props":3170,"children":3171},{},[3172,3177],{"type":24,"tag":1975,"props":3173,"children":3174},{},[3175],{"type":33,"value":3176},"What happens downstream?",{"type":24,"tag":1975,"props":3178,"children":3179},{},[3180,3182,3188],{"type":33,"value":3181},"Is an ",{"type":24,"tag":14,"props":3183,"children":3185},{"className":3184},[],[3186],{"type":33,"value":3187},"OrderCancelled",{"type":33,"value":3189}," event part of the contract?",{"type":24,"tag":29,"props":3191,"children":3192},{},[3193,3195,3200,3202,3207],{"type":33,"value":3194},"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":3196,"children":3198},{"className":3197},[],[3199],{"type":33,"value":3140},{"type":33,"value":3201}," order; a shipped order cannot be cancelled; repeating a successful request returns the cancelled state; and one logical ",{"type":24,"tag":14,"props":3203,"children":3205},{"className":3204},[],[3206],{"type":33,"value":3187},{"type":33,"value":3208}," event is produced for the transition.",{"type":24,"tag":29,"props":3210,"children":3211},{},[3212],{"type":33,"value":3213},"The goal is now specific enough to propose a change.",{"type":24,"tag":36,"props":3215,"children":3217},{"id":3216},"create-a-change-that-explains-why",[3218],{"type":33,"value":3219},"Create a change that explains why",{"type":24,"tag":29,"props":3221,"children":3222},{},[3223],{"type":33,"value":3224},"With OpenSpec initialized in the project, create a named change:",{"type":24,"tag":151,"props":3226,"children":3228},{"className":1535,"code":3227,"language":1537,"meta":7,"style":7},"openspec new change add-order-cancellation \\\n  --goal \"Customers can cancel their pending orders safely\"\nopenspec status --change add-order-cancellation\n",[3229],{"type":24,"tag":14,"props":3230,"children":3231},{"__ignoreMap":7},[3232,3258,3271],{"type":24,"tag":160,"props":3233,"children":3234},{"class":162,"line":163},[3235,3239,3243,3248,3253],{"type":24,"tag":160,"props":3236,"children":3237},{"style":173},[3238],{"type":33,"value":2329},{"type":24,"tag":160,"props":3240,"children":3241},{"style":238},[3242],{"type":33,"value":388},{"type":24,"tag":160,"props":3244,"children":3245},{"style":238},[3246],{"type":33,"value":3247}," change",{"type":24,"tag":160,"props":3249,"children":3250},{"style":238},[3251],{"type":33,"value":3252}," add-order-cancellation",{"type":24,"tag":160,"props":3254,"children":3255},{"style":640},[3256],{"type":33,"value":3257}," \\\n",{"type":24,"tag":160,"props":3259,"children":3260},{"class":162,"line":195},[3261,3266],{"type":24,"tag":160,"props":3262,"children":3263},{"style":640},[3264],{"type":33,"value":3265},"  --goal",{"type":24,"tag":160,"props":3267,"children":3268},{"style":238},[3269],{"type":33,"value":3270}," \"Customers can cancel their pending orders safely\"\n",{"type":24,"tag":160,"props":3272,"children":3273},{"class":162,"line":219},[3274,3278,3283,3288],{"type":24,"tag":160,"props":3275,"children":3276},{"style":173},[3277],{"type":33,"value":2329},{"type":24,"tag":160,"props":3279,"children":3280},{"style":238},[3281],{"type":33,"value":3282}," status",{"type":24,"tag":160,"props":3284,"children":3285},{"style":640},[3286],{"type":33,"value":3287}," --change",{"type":24,"tag":160,"props":3289,"children":3290},{"style":238},[3291],{"type":33,"value":3292}," add-order-cancellation\n",{"type":24,"tag":29,"props":3294,"children":3295},{},[3296,3298,3304,3306,3312,3314,3320,3322,3326,3328,3333,3335,3341,3343,3347,3349,3355,3357,3364],{"type":33,"value":3297},"The CLI creates ",{"type":24,"tag":14,"props":3299,"children":3301},{"className":3300},[],[3302],{"type":33,"value":3303},"openspec/changes/add-order-cancellation/",{"type":33,"value":3305},". In OpenSpec's default ",{"type":24,"tag":14,"props":3307,"children":3309},{"className":3308},[],[3310],{"type":33,"value":3311},"spec-driven",{"type":33,"value":3313}," workflow, ",{"type":24,"tag":14,"props":3315,"children":3317},{"className":3316},[],[3318],{"type":33,"value":3319},"proposal.md",{"type":33,"value":3321}," captures ",{"type":24,"tag":56,"props":3323,"children":3324},{},[3325],{"type":33,"value":2462},{"type":33,"value":3327},", a spec delta captures ",{"type":24,"tag":56,"props":3329,"children":3330},{},[3331],{"type":33,"value":3332},"what behavior changes",{"type":33,"value":3334},", ",{"type":24,"tag":14,"props":3336,"children":3338},{"className":3337},[],[3339],{"type":33,"value":3340},"design.md",{"type":33,"value":3342}," records ",{"type":24,"tag":56,"props":3344,"children":3345},{},[3346],{"type":33,"value":2753},{"type":33,"value":3348}," where a design is needed, and ",{"type":24,"tag":14,"props":3350,"children":3352},{"className":3351},[],[3353],{"type":33,"value":3354},"tasks.md",{"type":33,"value":3356}," tracks implementation. Proposal comes first; spec and design can follow in either order; tasks depend on both. The ",{"type":24,"tag":1265,"props":3358,"children":3361},{"href":3359,"rel":3360},"https://openspec.dev/docs/schemas/spec-driven",[1269],[3362],{"type":33,"value":3363},"schema reference",{"type":33,"value":3365}," describes those roles.",{"type":24,"tag":29,"props":3367,"children":3368},{},[3369],{"type":33,"value":3370},"The proposal should make the problem and scope clear without pretending to be the implementation plan. A shortened example:",{"type":24,"tag":151,"props":3372,"children":3374},{"className":2196,"code":3373,"language":1330,"meta":7,"style":7},"# 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",[3375],{"type":24,"tag":14,"props":3376,"children":3377},{"__ignoreMap":7},[3378,3386,3393,3401,3409,3416,3424,3432,3440,3448,3455,3463,3471,3479,3486,3494],{"type":24,"tag":160,"props":3379,"children":3380},{"class":162,"line":163},[3381],{"type":24,"tag":160,"props":3382,"children":3383},{},[3384],{"type":33,"value":3385},"# Proposal\n",{"type":24,"tag":160,"props":3387,"children":3388},{"class":162,"line":195},[3389],{"type":24,"tag":160,"props":3390,"children":3391},{"emptyLinePlaceholder":296},[3392],{"type":33,"value":299},{"type":24,"tag":160,"props":3394,"children":3395},{"class":162,"line":219},[3396],{"type":24,"tag":160,"props":3397,"children":3398},{},[3399],{"type":33,"value":3400},"## Why\n",{"type":24,"tag":160,"props":3402,"children":3403},{"class":162,"line":274},[3404],{"type":24,"tag":160,"props":3405,"children":3406},{},[3407],{"type":33,"value":3408},"Customers cannot cancel an eligible order without contacting support.\n",{"type":24,"tag":160,"props":3410,"children":3411},{"class":162,"line":283},[3412],{"type":24,"tag":160,"props":3413,"children":3414},{"emptyLinePlaceholder":296},[3415],{"type":33,"value":299},{"type":24,"tag":160,"props":3417,"children":3418},{"class":162,"line":292},[3419],{"type":24,"tag":160,"props":3420,"children":3421},{},[3422],{"type":33,"value":3423},"## What Changes\n",{"type":24,"tag":160,"props":3425,"children":3426},{"class":162,"line":302},[3427],{"type":24,"tag":160,"props":3428,"children":3429},{},[3430],{"type":33,"value":3431},"- Allow an authenticated owner to cancel a pending order.\n",{"type":24,"tag":160,"props":3433,"children":3434},{"class":162,"line":325},[3435],{"type":24,"tag":160,"props":3436,"children":3437},{},[3438],{"type":33,"value":3439},"- Preserve a single logical cancellation when requests are repeated.\n",{"type":24,"tag":160,"props":3441,"children":3442},{"class":162,"line":372},[3443],{"type":24,"tag":160,"props":3444,"children":3445},{},[3446],{"type":33,"value":3447},"- Publish the cancellation for downstream consumers.\n",{"type":24,"tag":160,"props":3449,"children":3450},{"class":162,"line":400},[3451],{"type":24,"tag":160,"props":3452,"children":3453},{"emptyLinePlaceholder":296},[3454],{"type":33,"value":299},{"type":24,"tag":160,"props":3456,"children":3457},{"class":162,"line":418},[3458],{"type":24,"tag":160,"props":3459,"children":3460},{},[3461],{"type":33,"value":3462},"## Capabilities\n",{"type":24,"tag":160,"props":3464,"children":3465},{"class":162,"line":426},[3466],{"type":24,"tag":160,"props":3467,"children":3468},{},[3469],{"type":33,"value":3470},"### New Capabilities\n",{"type":24,"tag":160,"props":3472,"children":3473},{"class":162,"line":2874},[3474],{"type":24,"tag":160,"props":3475,"children":3476},{},[3477],{"type":33,"value":3478},"- `order-cancellation`: Rules and outcomes of cancelling an order.\n",{"type":24,"tag":160,"props":3480,"children":3481},{"class":162,"line":2883},[3482],{"type":24,"tag":160,"props":3483,"children":3484},{"emptyLinePlaceholder":296},[3485],{"type":33,"value":299},{"type":24,"tag":160,"props":3487,"children":3488},{"class":162,"line":2892},[3489],{"type":24,"tag":160,"props":3490,"children":3491},{},[3492],{"type":33,"value":3493},"## Impact\n",{"type":24,"tag":160,"props":3495,"children":3496},{"class":162,"line":2901},[3497],{"type":24,"tag":160,"props":3498,"children":3499},{},[3500],{"type":33,"value":3501},"Order API, order persistence, cancellation event, and contract tests.\n",{"type":24,"tag":29,"props":3503,"children":3504},{},[3505],{"type":33,"value":3506},"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":36,"props":3508,"children":3510},{"id":3509},"put-observable-behavior-in-the-spec",[3511],{"type":33,"value":3512},"Put observable behavior in the spec",{"type":24,"tag":29,"props":3514,"children":3515},{},[3516,3518,3524],{"type":33,"value":3517},"The delta spec belongs under the capability named in the proposal, for example ",{"type":24,"tag":14,"props":3519,"children":3521},{"className":3520},[],[3522],{"type":33,"value":3523},"openspec/changes/add-order-cancellation/specs/order-cancellation/spec.md",{"type":33,"value":3525},". It describes the behavior that changes, not the classes or SQL statements that happen to implement it.",{"type":24,"tag":29,"props":3527,"children":3528},{},[3529],{"type":33,"value":3530},"Here is an illustrative excerpt in OpenSpec's requirement-and-scenario format:",{"type":24,"tag":151,"props":3532,"children":3534},{"className":2196,"code":3533,"language":1330,"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",[3535],{"type":24,"tag":14,"props":3536,"children":3537},{"__ignoreMap":7},[3538,3546,3553,3561,3569,3577,3584,3592,3599,3607,3615,3622,3630,3638,3646,3653,3661,3670,3679,3687,3696,3705,3713,3721,3730,3739,3747,3756,3765],{"type":24,"tag":160,"props":3539,"children":3540},{"class":162,"line":163},[3541],{"type":24,"tag":160,"props":3542,"children":3543},{},[3544],{"type":33,"value":3545},"# Spec Delta\n",{"type":24,"tag":160,"props":3547,"children":3548},{"class":162,"line":195},[3549],{"type":24,"tag":160,"props":3550,"children":3551},{"emptyLinePlaceholder":296},[3552],{"type":33,"value":299},{"type":24,"tag":160,"props":3554,"children":3555},{"class":162,"line":219},[3556],{"type":24,"tag":160,"props":3557,"children":3558},{},[3559],{"type":33,"value":3560},"## Purpose\n",{"type":24,"tag":160,"props":3562,"children":3563},{"class":162,"line":274},[3564],{"type":24,"tag":160,"props":3565,"children":3566},{},[3567],{"type":33,"value":3568},"Define when the owner of an order may cancel it and how the service\n",{"type":24,"tag":160,"props":3570,"children":3571},{"class":162,"line":283},[3572],{"type":24,"tag":160,"props":3573,"children":3574},{},[3575],{"type":33,"value":3576},"responds to rejected or repeated cancellation requests.\n",{"type":24,"tag":160,"props":3578,"children":3579},{"class":162,"line":292},[3580],{"type":24,"tag":160,"props":3581,"children":3582},{"emptyLinePlaceholder":296},[3583],{"type":33,"value":299},{"type":24,"tag":160,"props":3585,"children":3586},{"class":162,"line":302},[3587],{"type":24,"tag":160,"props":3588,"children":3589},{},[3590],{"type":33,"value":3591},"## ADDED Requirements\n",{"type":24,"tag":160,"props":3593,"children":3594},{"class":162,"line":325},[3595],{"type":24,"tag":160,"props":3596,"children":3597},{"emptyLinePlaceholder":296},[3598],{"type":33,"value":299},{"type":24,"tag":160,"props":3600,"children":3601},{"class":162,"line":372},[3602],{"type":24,"tag":160,"props":3603,"children":3604},{},[3605],{"type":33,"value":3606},"### Requirement: Cancel an eligible order\n",{"type":24,"tag":160,"props":3608,"children":3609},{"class":162,"line":400},[3610],{"type":24,"tag":160,"props":3611,"children":3612},{},[3613],{"type":33,"value":3614},"The service MUST allow the authenticated owner to cancel a PENDING order.\n",{"type":24,"tag":160,"props":3616,"children":3617},{"class":162,"line":418},[3618],{"type":24,"tag":160,"props":3619,"children":3620},{"emptyLinePlaceholder":296},[3621],{"type":33,"value":299},{"type":24,"tag":160,"props":3623,"children":3624},{"class":162,"line":426},[3625],{"type":24,"tag":160,"props":3626,"children":3627},{},[3628],{"type":33,"value":3629},"#### Scenario: Eligible order\n",{"type":24,"tag":160,"props":3631,"children":3632},{"class":162,"line":2874},[3633],{"type":24,"tag":160,"props":3634,"children":3635},{},[3636],{"type":33,"value":3637},"- **WHEN** the owner requests cancellation of a PENDING order\n",{"type":24,"tag":160,"props":3639,"children":3640},{"class":162,"line":2883},[3641],{"type":24,"tag":160,"props":3642,"children":3643},{},[3644],{"type":33,"value":3645},"- **THEN** the order becomes CANCELLED and one logical OrderCancelled event is recorded\n",{"type":24,"tag":160,"props":3647,"children":3648},{"class":162,"line":2892},[3649],{"type":24,"tag":160,"props":3650,"children":3651},{"emptyLinePlaceholder":296},[3652],{"type":33,"value":299},{"type":24,"tag":160,"props":3654,"children":3655},{"class":162,"line":2901},[3656],{"type":24,"tag":160,"props":3657,"children":3658},{},[3659],{"type":33,"value":3660},"#### Scenario: Shipment has started\n",{"type":24,"tag":160,"props":3662,"children":3664},{"class":162,"line":3663},17,[3665],{"type":24,"tag":160,"props":3666,"children":3667},{},[3668],{"type":33,"value":3669},"- **WHEN** the owner requests cancellation of a SHIPPED order\n",{"type":24,"tag":160,"props":3671,"children":3673},{"class":162,"line":3672},18,[3674],{"type":24,"tag":160,"props":3675,"children":3676},{},[3677],{"type":33,"value":3678},"- **THEN** the service rejects the request and leaves the order unchanged\n",{"type":24,"tag":160,"props":3680,"children":3682},{"class":162,"line":3681},19,[3683],{"type":24,"tag":160,"props":3684,"children":3685},{"emptyLinePlaceholder":296},[3686],{"type":33,"value":299},{"type":24,"tag":160,"props":3688,"children":3690},{"class":162,"line":3689},20,[3691],{"type":24,"tag":160,"props":3692,"children":3693},{},[3694],{"type":33,"value":3695},"#### Scenario: Not the owner\n",{"type":24,"tag":160,"props":3697,"children":3699},{"class":162,"line":3698},21,[3700],{"type":24,"tag":160,"props":3701,"children":3702},{},[3703],{"type":33,"value":3704},"- **WHEN** a different authenticated customer requests cancellation\n",{"type":24,"tag":160,"props":3706,"children":3708},{"class":162,"line":3707},22,[3709],{"type":24,"tag":160,"props":3710,"children":3711},{},[3712],{"type":33,"value":3678},{"type":24,"tag":160,"props":3714,"children":3716},{"class":162,"line":3715},23,[3717],{"type":24,"tag":160,"props":3718,"children":3719},{"emptyLinePlaceholder":296},[3720],{"type":33,"value":299},{"type":24,"tag":160,"props":3722,"children":3724},{"class":162,"line":3723},24,[3725],{"type":24,"tag":160,"props":3726,"children":3727},{},[3728],{"type":33,"value":3729},"### Requirement: Repeat a completed cancellation\n",{"type":24,"tag":160,"props":3731,"children":3733},{"class":162,"line":3732},25,[3734],{"type":24,"tag":160,"props":3735,"children":3736},{},[3737],{"type":33,"value":3738},"The service MUST handle repeated requests without creating a second cancellation.\n",{"type":24,"tag":160,"props":3740,"children":3742},{"class":162,"line":3741},26,[3743],{"type":24,"tag":160,"props":3744,"children":3745},{"emptyLinePlaceholder":296},[3746],{"type":33,"value":299},{"type":24,"tag":160,"props":3748,"children":3750},{"class":162,"line":3749},27,[3751],{"type":24,"tag":160,"props":3752,"children":3753},{},[3754],{"type":33,"value":3755},"#### Scenario: Repeated request\n",{"type":24,"tag":160,"props":3757,"children":3759},{"class":162,"line":3758},28,[3760],{"type":24,"tag":160,"props":3761,"children":3762},{},[3763],{"type":33,"value":3764},"- **WHEN** the owner repeats a successful cancellation request\n",{"type":24,"tag":160,"props":3766,"children":3768},{"class":162,"line":3767},29,[3769],{"type":24,"tag":160,"props":3770,"children":3771},{},[3772],{"type":33,"value":3773},"- **THEN** the service returns the existing CANCELLED state without a new transition\n",{"type":24,"tag":29,"props":3775,"children":3776},{},[3777,3779,3785],{"type":33,"value":3778},"This still leaves questions for the API contract: should a shipped order return ",{"type":24,"tag":14,"props":3780,"children":3782},{"className":3781},[],[3783],{"type":33,"value":3784},"409 Conflict",{"type":33,"value":3786},"? 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":29,"props":3788,"children":3789},{},[3790,3792,3798,3800,3806],{"type":33,"value":3791},"Keep the wording at the level of behavior. “Use optimistic locking in ",{"type":24,"tag":14,"props":3793,"children":3795},{"className":3794},[],[3796],{"type":33,"value":3797},"OrderRepository",{"type":33,"value":3799},"” 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":1265,"props":3801,"children":3803},{"href":3359,"rel":3802},[1269],[3804],{"type":33,"value":3805},"spec guidance",{"type":33,"value":3807}," makes the same distinction.",{"type":24,"tag":36,"props":3809,"children":3811},{"id":3810},"use-the-design-for-technical-trade-offs",[3812],{"type":33,"value":3813},"Use the design for technical trade-offs",{"type":24,"tag":29,"props":3815,"children":3816},{},[3817],{"type":33,"value":3818},"The design connects those requirements to the actual system. For this example, it might record:",{"type":24,"tag":151,"props":3820,"children":3822},{"className":2196,"code":3821,"language":1330,"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",[3823],{"type":24,"tag":14,"props":3824,"children":3825},{"__ignoreMap":7},[3826,3834,3841,3849,3857,3865,3873,3880,3888,3896],{"type":24,"tag":160,"props":3827,"children":3828},{"class":162,"line":163},[3829],{"type":24,"tag":160,"props":3830,"children":3831},{},[3832],{"type":33,"value":3833},"# Design\n",{"type":24,"tag":160,"props":3835,"children":3836},{"class":162,"line":195},[3837],{"type":24,"tag":160,"props":3838,"children":3839},{"emptyLinePlaceholder":296},[3840],{"type":33,"value":299},{"type":24,"tag":160,"props":3842,"children":3843},{"class":162,"line":219},[3844],{"type":24,"tag":160,"props":3845,"children":3846},{},[3847],{"type":33,"value":3848},"## Decisions\n",{"type":24,"tag":160,"props":3850,"children":3851},{"class":162,"line":274},[3852],{"type":24,"tag":160,"props":3853,"children":3854},{},[3855],{"type":33,"value":3856},"- Use an atomic conditional update to change PENDING to CANCELLED.\n",{"type":24,"tag":160,"props":3858,"children":3859},{"class":162,"line":283},[3860],{"type":24,"tag":160,"props":3861,"children":3862},{},[3863],{"type":33,"value":3864},"- Record the OrderCancelled event in the same database transaction.\n",{"type":24,"tag":160,"props":3866,"children":3867},{"class":162,"line":292},[3868],{"type":24,"tag":160,"props":3869,"children":3870},{},[3871],{"type":33,"value":3872},"- On a lost race, reload the order and return its current state when it is CANCELLED.\n",{"type":24,"tag":160,"props":3874,"children":3875},{"class":162,"line":302},[3876],{"type":24,"tag":160,"props":3877,"children":3878},{"emptyLinePlaceholder":296},[3879],{"type":33,"value":299},{"type":24,"tag":160,"props":3881,"children":3882},{"class":162,"line":325},[3883],{"type":24,"tag":160,"props":3884,"children":3885},{},[3886],{"type":33,"value":3887},"## Trade-offs\n",{"type":24,"tag":160,"props":3889,"children":3890},{"class":162,"line":372},[3891],{"type":24,"tag":160,"props":3892,"children":3893},{},[3894],{"type":33,"value":3895},"- A conditional update avoids holding a database lock across the request.\n",{"type":24,"tag":160,"props":3897,"children":3898},{"class":162,"line":400},[3899],{"type":24,"tag":160,"props":3900,"children":3901},{},[3902],{"type":33,"value":3903},"- The event must be durable even if the process stops after commit.\n",{"type":24,"tag":29,"props":3905,"children":3906},{},[3907,3909,3915],{"type":33,"value":3908},"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":1265,"props":3910,"children":3912},{"href":3911},"/transactional-outbox/",[3913],{"type":33,"value":3914},"Transactional Outbox",{"type":33,"value":3916}," pattern is one way to make state and publication intent atomic.",{"type":24,"tag":29,"props":3918,"children":3919},{},[3920,3922,3927],{"type":33,"value":3921},"Design is not compulsory paperwork for every change. OpenSpec can omit ",{"type":24,"tag":14,"props":3923,"children":3925},{"className":3924},[],[3926],{"type":33,"value":3340},{"type":33,"value":3928}," 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":36,"props":3930,"children":3932},{"id":3931},"turn-the-plan-into-work-you-can-verify",[3933],{"type":33,"value":3934},"Turn the plan into work you can verify",{"type":24,"tag":29,"props":3936,"children":3937},{},[3938,3943],{"type":24,"tag":14,"props":3939,"children":3941},{"className":3940},[],[3942],{"type":33,"value":3354},{"type":33,"value":3944}," turns the spec and design into an implementation checklist. Each item should end in an observable result:",{"type":24,"tag":151,"props":3946,"children":3948},{"className":2196,"code":3947,"language":1330,"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",[3949],{"type":24,"tag":14,"props":3950,"children":3951},{"__ignoreMap":7},[3952,3960,3968,3976],{"type":24,"tag":160,"props":3953,"children":3954},{"class":162,"line":163},[3955],{"type":24,"tag":160,"props":3956,"children":3957},{},[3958],{"type":33,"value":3959},"- [ ] Add the cancellation API and authorization checks.\n",{"type":24,"tag":160,"props":3961,"children":3962},{"class":162,"line":195},[3963],{"type":24,"tag":160,"props":3964,"children":3965},{},[3966],{"type":33,"value":3967},"- [ ] Persist the order transition and event intent atomically.\n",{"type":24,"tag":160,"props":3969,"children":3970},{"class":162,"line":219},[3971],{"type":24,"tag":160,"props":3972,"children":3973},{},[3974],{"type":33,"value":3975},"- [ ] Test eligible, shipped, unauthorized, and repeated requests.\n",{"type":24,"tag":160,"props":3977,"children":3978},{"class":162,"line":274},[3979],{"type":24,"tag":160,"props":3980,"children":3981},{},[3982],{"type":33,"value":3983},"- [ ] Run the project checks and review the API response contract.\n",{"type":24,"tag":29,"props":3985,"children":3986},{},[3987],{"type":33,"value":3988},"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":29,"props":3990,"children":3991},{},[3992,3994,4001],{"type":33,"value":3993},"Before code starts, review the three questions OpenSpec's ",{"type":24,"tag":1265,"props":3995,"children":3998},{"href":3996,"rel":3997},"https://openspec.dev/docs/quickstart",[1269],[3999],{"type":33,"value":4000},"quickstart",{"type":33,"value":4002}," 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":36,"props":4004,"children":4006},{"id":4005},"give-the-agent-a-bounded-handoff",[4007],{"type":33,"value":4008},"Give the agent a bounded handoff",{"type":24,"tag":29,"props":4010,"children":4011},{},[4012],{"type":33,"value":4013},"Once the artifacts are agreed, the implementation request can be short:",{"type":24,"tag":151,"props":4015,"children":4017},{"className":1446,"code":4016,"language":33,"meta":7,"style":7},"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",[4018],{"type":24,"tag":14,"props":4019,"children":4020},{"__ignoreMap":7},[4021,4029,4037,4045],{"type":24,"tag":160,"props":4022,"children":4023},{"class":162,"line":163},[4024],{"type":24,"tag":160,"props":4025,"children":4026},{},[4027],{"type":33,"value":4028},"Apply the add-order-cancellation change. Read its proposal, spec,\n",{"type":24,"tag":160,"props":4030,"children":4031},{"class":162,"line":195},[4032],{"type":24,"tag":160,"props":4033,"children":4034},{},[4035],{"type":33,"value":4036},"design, and tasks before editing code. Implement one task at a time.\n",{"type":24,"tag":160,"props":4038,"children":4039},{"class":162,"line":219},[4040],{"type":24,"tag":160,"props":4041,"children":4042},{},[4043],{"type":33,"value":4044},"Run the relevant tests and report any requirement that is ambiguous\n",{"type":24,"tag":160,"props":4046,"children":4047},{"class":162,"line":274},[4048],{"type":24,"tag":160,"props":4049,"children":4050},{},[4051],{"type":33,"value":4052},"or cannot be met by the current design. Do not invent a new business rule.\n",{"type":24,"tag":29,"props":4054,"children":4055},{},[4056,4058,4063,4065,4070,4072,4078],{"type":33,"value":4057},"OpenSpec calls this the ",{"type":24,"tag":56,"props":4059,"children":4060},{},[4061],{"type":33,"value":4062},"apply",{"type":33,"value":4064}," phase. It tracks progress in the checkboxes in ",{"type":24,"tag":14,"props":4066,"children":4068},{"className":4067},[],[4069],{"type":33,"value":3354},{"type":33,"value":4071},"; it is not a promise that the CLI has turned the spec into correct code. The ",{"type":24,"tag":1265,"props":4073,"children":4075},{"href":3996,"rel":4074},[1269],[4076],{"type":33,"value":4077},"OpenSpec workflow",{"type":33,"value":4079}," explicitly allows the artifacts to be corrected while work is underway.",{"type":24,"tag":29,"props":4081,"children":4082},{},[4083,4085,4091],{"type":33,"value":4084},"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":1265,"props":4086,"children":4088},{"href":4087},"/git-worktrees-parallel-development/",[4089],{"type":33,"value":4090},"Git worktree",{"type":33,"value":4092}," and keep ownership of integration clear.",{"type":24,"tag":36,"props":4094,"children":4096},{"id":4095},"when-implementation-reveals-a-missing-scenario",[4097],{"type":33,"value":4098},"When implementation reveals a missing scenario",{"type":24,"tag":29,"props":4100,"children":4101},{},[4102,4104,4109,4111,4116],{"type":33,"value":4103},"Suppose a test exposes a race: two cancellation requests read the same ",{"type":24,"tag":14,"props":4105,"children":4107},{"className":4106},[],[4108],{"type":33,"value":3140},{"type":33,"value":4110}," order before either writes. The original repeated-request scenario covers a request made ",{"type":24,"tag":1356,"props":4112,"children":4113},{},[4114],{"type":33,"value":4115},"after",{"type":33,"value":4117}," cancellation, but does not say what simultaneous requests should do.",{"type":24,"tag":29,"props":4119,"children":4120},{},[4121],{"type":33,"value":4122},"Do not silently choose the easiest behavior in code. Add the missing scenario to the spec:",{"type":24,"tag":151,"props":4124,"children":4126},{"className":2196,"code":4125,"language":1330,"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",[4127],{"type":24,"tag":14,"props":4128,"children":4129},{"__ignoreMap":7},[4130,4138,4146,4154],{"type":24,"tag":160,"props":4131,"children":4132},{"class":162,"line":163},[4133],{"type":24,"tag":160,"props":4134,"children":4135},{},[4136],{"type":33,"value":4137},"#### Scenario: Concurrent cancellation requests\n",{"type":24,"tag":160,"props":4139,"children":4140},{"class":162,"line":195},[4141],{"type":24,"tag":160,"props":4142,"children":4143},{},[4144],{"type":33,"value":4145},"- **WHEN** two requests from the owner try to cancel the same PENDING order\n",{"type":24,"tag":160,"props":4147,"children":4148},{"class":162,"line":219},[4149],{"type":24,"tag":160,"props":4150,"children":4151},{},[4152],{"type":33,"value":4153},"- **THEN** exactly one cancellation transition and one logical event are recorded\n",{"type":24,"tag":160,"props":4155,"children":4156},{"class":162,"line":274},[4157],{"type":24,"tag":160,"props":4158,"children":4159},{},[4160],{"type":33,"value":4161},"- **AND** both requests can observe the final CANCELLED state\n",{"type":24,"tag":29,"props":4163,"children":4164},{},[4165,4167,4172],{"type":33,"value":4166},"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":56,"props":4168,"children":4169},{},[4170],{"type":33,"value":4171},"the spec changes when understanding changes",{"type":33,"value":4173},". It should describe the behavior the team intends to ship, not preserve an early guess for appearances.",{"type":24,"tag":36,"props":4175,"children":4177},{"id":4176},"verify-the-implementation-then-archive-the-change",[4178],{"type":33,"value":4179},"Verify the implementation, then archive the change",{"type":24,"tag":29,"props":4181,"children":4182},{},[4183],{"type":33,"value":4184},"The final review needs evidence for each important scenario:",{"type":24,"tag":1940,"props":4186,"children":4187},{},[4188,4204],{"type":24,"tag":1944,"props":4189,"children":4190},{},[4191],{"type":24,"tag":1948,"props":4192,"children":4193},{},[4194,4199],{"type":24,"tag":1952,"props":4195,"children":4196},{},[4197],{"type":33,"value":4198},"Scenario",{"type":24,"tag":1952,"props":4200,"children":4201},{},[4202],{"type":33,"value":4203},"Useful evidence",{"type":24,"tag":1968,"props":4205,"children":4206},{},[4207,4220,4233,4245,4258,4271],{"type":24,"tag":1948,"props":4208,"children":4209},{},[4210,4215],{"type":24,"tag":1975,"props":4211,"children":4212},{},[4213],{"type":33,"value":4214},"Eligible owner",{"type":24,"tag":1975,"props":4216,"children":4217},{},[4218],{"type":33,"value":4219},"API test confirms the state transition and response",{"type":24,"tag":1948,"props":4221,"children":4222},{},[4223,4228],{"type":24,"tag":1975,"props":4224,"children":4225},{},[4226],{"type":33,"value":4227},"Shipped order",{"type":24,"tag":1975,"props":4229,"children":4230},{},[4231],{"type":33,"value":4232},"API test confirms rejection and unchanged state",{"type":24,"tag":1948,"props":4234,"children":4235},{},[4236,4241],{"type":24,"tag":1975,"props":4237,"children":4238},{},[4239],{"type":33,"value":4240},"Different customer",{"type":24,"tag":1975,"props":4242,"children":4243},{},[4244],{"type":33,"value":4232},{"type":24,"tag":1948,"props":4246,"children":4247},{},[4248,4253],{"type":24,"tag":1975,"props":4249,"children":4250},{},[4251],{"type":33,"value":4252},"Repeated request",{"type":24,"tag":1975,"props":4254,"children":4255},{},[4256],{"type":33,"value":4257},"Test confirms no second logical transition or event",{"type":24,"tag":1948,"props":4259,"children":4260},{},[4261,4266],{"type":24,"tag":1975,"props":4262,"children":4263},{},[4264],{"type":33,"value":4265},"Concurrent requests",{"type":24,"tag":1975,"props":4267,"children":4268},{},[4269],{"type":33,"value":4270},"Integration test confirms the chosen race behavior",{"type":24,"tag":1948,"props":4272,"children":4273},{},[4274,4279],{"type":24,"tag":1975,"props":4275,"children":4276},{},[4277],{"type":33,"value":4278},"Downstream event",{"type":24,"tag":1975,"props":4280,"children":4281},{},[4282],{"type":33,"value":4283},"Test confirms the event intent commits with the order change",{"type":24,"tag":29,"props":4285,"children":4286},{},[4287],{"type":33,"value":4288},"Run OpenSpec's structural validation too:",{"type":24,"tag":151,"props":4290,"children":4292},{"className":1535,"code":4291,"language":1537,"meta":7,"style":7},"openspec validate add-order-cancellation --strict\nopenspec status --change add-order-cancellation\n",[4293],{"type":24,"tag":14,"props":4294,"children":4295},{"__ignoreMap":7},[4296,4317],{"type":24,"tag":160,"props":4297,"children":4298},{"class":162,"line":163},[4299,4303,4308,4312],{"type":24,"tag":160,"props":4300,"children":4301},{"style":173},[4302],{"type":33,"value":2329},{"type":24,"tag":160,"props":4304,"children":4305},{"style":238},[4306],{"type":33,"value":4307}," validate",{"type":24,"tag":160,"props":4309,"children":4310},{"style":238},[4311],{"type":33,"value":3252},{"type":24,"tag":160,"props":4313,"children":4314},{"style":640},[4315],{"type":33,"value":4316}," --strict\n",{"type":24,"tag":160,"props":4318,"children":4319},{"class":162,"line":195},[4320,4324,4328,4332],{"type":24,"tag":160,"props":4321,"children":4322},{"style":173},[4323],{"type":33,"value":2329},{"type":24,"tag":160,"props":4325,"children":4326},{"style":238},[4327],{"type":33,"value":3282},{"type":24,"tag":160,"props":4329,"children":4330},{"style":640},[4331],{"type":33,"value":3287},{"type":24,"tag":160,"props":4333,"children":4334},{"style":238},[4335],{"type":33,"value":3292},{"type":24,"tag":29,"props":4337,"children":4338},{},[4339],{"type":33,"value":4340},"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":29,"props":4342,"children":4343},{},[4344,4346,4352,4354,4359],{"type":33,"value":4345},"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":4347,"children":4349},{"className":4348},[],[4350],{"type":33,"value":4351},"openspec/changes/archive/",{"type":33,"value":4353}," and merges the delta requirements into the main specs, so they describe the system as built. The ",{"type":24,"tag":1265,"props":4355,"children":4357},{"href":3996,"rel":4356},[1269],[4358],{"type":33,"value":4000},{"type":33,"value":4360}," 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":36,"props":4362,"children":4364},{"id":4363},"keep-the-method-proportional-to-the-change",[4365],{"type":33,"value":4366},"Keep the method proportional to the change",{"type":24,"tag":29,"props":4368,"children":4369},{},[4370,4372,4378],{"type":33,"value":4371},"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":4373,"children":4375},{"className":4374},[],[4376],{"type":33,"value":4377},"skip_specs: true",{"type":33,"value":4379}," 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":29,"props":4381,"children":4382},{},[4383],{"type":33,"value":4384},"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":36,"props":4386,"children":4387},{"id":1254},[4388],{"type":33,"value":4389},"Further reading",{"type":24,"tag":48,"props":4391,"children":4392},{},[4393,4403,4413],{"type":24,"tag":52,"props":4394,"children":4395},{},[4396,4402],{"type":24,"tag":1265,"props":4397,"children":4399},{"href":3996,"rel":4398},[1269],[4400],{"type":33,"value":4401},"OpenSpec: Quickstart",{"type":33,"value":2083},{"type":24,"tag":52,"props":4404,"children":4405},{},[4406,4412],{"type":24,"tag":1265,"props":4407,"children":4409},{"href":3359,"rel":4408},[1269],[4410],{"type":33,"value":4411},"OpenSpec: spec-driven schema",{"type":33,"value":2083},{"type":24,"tag":52,"props":4414,"children":4415},{},[4416,4423],{"type":24,"tag":1265,"props":4417,"children":4420},{"href":4418,"rel":4419},"https://openspec.dev/docs/cli",[1269],[4421],{"type":33,"value":4422},"OpenSpec: CLI reference",{"type":33,"value":2083},{"type":24,"tag":1308,"props":4425,"children":4426},{},[4427],{"type":33,"value":1312},{"title":7,"searchDepth":195,"depth":195,"links":4429},[4430,4431,4432,4433,4434,4435,4436,4437,4438,4439],{"id":3078,"depth":195,"text":3081},{"id":3216,"depth":195,"text":3219},{"id":3509,"depth":195,"text":3512},{"id":3810,"depth":195,"text":3813},{"id":3931,"depth":195,"text":3934},{"id":4005,"depth":195,"text":4008},{"id":4095,"depth":195,"text":4098},{"id":4176,"depth":195,"text":4179},{"id":4363,"depth":195,"text":4366},{"id":1254,"depth":195,"text":4389},"content:posts:spec-driven-development-openspec.md","posts/spec-driven-development-openspec.md","posts/spec-driven-development-openspec",[4444,4445,4446,4447,4451,4455,4459,4463,4467,4471,4475,4479,4483,4487,4491,4495,4496,4500,4504,4508,4512,4516,4520,4524,4528,4532,4536,4539,4543,4547],{"_path":1338,"title":1339,"date":1341},{"_path":1886,"title":1887,"date":1889},{"_path":3044,"title":3045,"date":3047},{"_path":4448,"title":4449,"date":4450},"/posts/git-worktrees-parallel-development","Git Worktrees: Parallel Work Without Clobbering Your Workspace","2025-07-15T11:00:00.000Z",{"_path":4452,"title":4453,"date":4454},"/posts/transactional-outbox","Transactional Outbox: Saving State Without Losing Events","2025-06-15T11:00:00.000Z",{"_path":4456,"title":4457,"date":4458},"/posts/api-evolution-strategy","Your API Needs an Evolution Strategy","2025-05-15T11:00:00.000Z",{"_path":4460,"title":4461,"date":4462},"/posts/n-plus-one-queries","The N+1 Query Problem: Finding and Fixing a Slow Search Endpoint","2025-04-15T11:00:00.000Z",{"_path":4464,"title":4465,"date":4466},"/posts/relational-vs-nosql-databases","Relational vs. NoSQL Databases: Why PostgreSQL Is My Default","2025-03-15T11:00:00.000Z",{"_path":4468,"title":4469,"date":4470},"/posts/hidden-cost-event-driven-architectures","The Hidden Cost of Event-Driven Architectures","2025-02-15T11:00:00.000Z",{"_path":4472,"title":4473,"date":4474},"/posts/gpt-sql-integration","Natural Language to SQL with GPT: Build a Safer Query Interface","2025-01-15T11:00:00.000Z",{"_path":4476,"title":4477,"date":4478},"/posts/java-security-best-practices","Java Security Best Practices: Preventing Vulnerabilities and Threats","2024-08-15T11:00:00.000Z",{"_path":4480,"title":4481,"date":4482},"/posts/circuit-breaking-resilience","Circuit Breaking in Distributed Systems","2024-07-15T11:00:00.000Z",{"_path":4484,"title":4485,"date":4486},"/posts/maven-and-github-actions","Automating Java Builds with Maven and GitHub Actions CI/CD","2024-06-15T11:00:00.000Z",{"_path":4488,"title":4489,"date":4490},"/posts/pattern-matching-sealed-classes-java21","A Guide to Pattern Matching and Sealed Classes in Java 21","2024-05-15T11:00:00.000Z",{"_path":4492,"title":4493,"date":4494},"/posts/async-java-completable-vs-reactive","Asynchronous Programming in Java: CompletableFuture vs Reactive Streams","2024-04-15T11:00:00.000Z",{"_path":4,"title":8,"date":10},{"_path":4497,"title":4498,"date":4499},"/posts/microservices-mistakes","Common Mistakes When Developing Microservices","2024-02-15T11:00:00.000Z",{"_path":4501,"title":4502,"date":4503},"/posts/tdd-testing","Test-Driven Development (TDD) in Java with JUnit and Mockito","2024-01-15T11:00:00.000Z",{"_path":4505,"title":4506,"date":4507},"/posts/single-table-design","Single Table Design","2023-12-27T11:00:00.000Z",{"_path":4509,"title":4510,"date":4511},"/posts/java-17","Exploring Java 17's New Features","2023-10-08T11:00:00.000Z",{"_path":4513,"title":4514,"date":4515},"/posts/hexagonal-architecture","Introduction to Hexagonal Architecture","2023-10-02 18:00:00",{"_path":4517,"title":4518,"date":4519},"/posts/terraform","Unleashing the Power of Infrastructure as Code (IaC)","2023-09-24 18:00:00",{"_path":4521,"title":4522,"date":4523},"/posts/python-virtual-environments","Python Virtual Environments (venv)","2023-09-17 22:00:00",{"_path":4525,"title":4526,"date":4527},"/posts/api-first-principles","API First principles","2023-09-01 22:15:18",{"_path":4529,"title":4530,"date":4531},"/posts/how-to-configure-ssh-key-based-authentication","How To Configure SSH Key-Based Authentication","2021-02-21 03:46:12",{"_path":4533,"title":4534,"date":4535},"/posts/algorithms-binary-search-tree","Algorithms - Binary Search Tree in kotlin","2020-12-06T11:00:00.000Z",{"_path":4537,"title":4538,"date":4535},"/posts/how-to-create-a-twitter-bot","How to create a Twitter bot",{"_path":4540,"title":4541,"date":4542},"/posts/manage-aws-infrastructure-with-terraform","Bootstrap complete Java application infrastructure in AWS with Terraform","2020-12-02T11:00:00.000Z",{"_path":4544,"title":4545,"date":4546},"/posts/algorithms-working-with-trees","Algorithms - Binary tree traversals in kotlin","2020-11-24T11:00:00.000Z",{"_path":4548,"title":4549,"date":4550},"/posts/playing-with-ocr-libraries","Playing with OCR libraries","2020-11-22T11:00:00.000Z",1791700921698]