<?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: David Silva</title>
    <description>The latest articles on DEV Community by David Silva (@davidslv).</description>
    <link>https://dev.to/davidslv</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%2F3624184%2F01c672e3-dd69-4325-9522-1e1501b91553.jpeg</url>
      <title>DEV Community: David Silva</title>
      <link>https://dev.to/davidslv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidslv"/>
    <language>en</language>
    <item>
      <title>Comprehension Debt: The Hidden Cost of AI-Generated Code</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Sun, 17 May 2026 17:24:36 +0000</pubDate>
      <link>https://dev.to/davidslv/comprehension-debt-the-hidden-cost-of-ai-generated-code-15kj</link>
      <guid>https://dev.to/davidslv/comprehension-debt-the-hidden-cost-of-ai-generated-code-15kj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Comprehension Debt: The Hidden Cost of AI-Generated Code&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You're not a developer anymore. You're a reviewer of code you don't understand."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That line from &lt;a href="https://www.youtube.com/@TheSeriousCTO" rel="noopener noreferrer"&gt;The Serious CTO&lt;/a&gt; named something I’d been feeling but couldn’t articulate. With AI, the volume of code we ship has completely decoupled from the amount of code any human actually understands.&lt;/p&gt;

&lt;p&gt;I call this &lt;strong&gt;comprehension debt&lt;/strong&gt; — the gap between the code in our systems and the knowledge in our teams’ heads. And I believe it’s the most important kind of debt our industry has accumulated in the past two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s not technical debt
&lt;/h2&gt;

&lt;p&gt;Ward Cunningham’s original “technical debt” was a conscious trade-off. Comprehension debt is different.&lt;/p&gt;

&lt;p&gt;It isn’t deliberate.&lt;br&gt;&lt;br&gt;
It isn’t local.&lt;br&gt;&lt;br&gt;
And it doesn’t show up on any dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the dashboards actually measure
&lt;/h2&gt;

&lt;p&gt;DORA, Flow, and even the new AI Impact metrics all measure &lt;strong&gt;motion&lt;/strong&gt; — speed, volume, acceptance rates, deployment frequency. None of them measure whether anyone on the team can still explain the code they shipped last quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard answers fall short
&lt;/h2&gt;

&lt;p&gt;More code review, more linters, more tests, and “future AI will fix it” don’t solve comprehension debt. They mostly create the illusion of safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Fields nobody can explain, controllers with dead branches, 2am incidents where the on-call engineer can read the stack trace but can’t narrate the logic. Individually minor. Collectively, the new shape of legacy code — built at unprecedented speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should we do instead?
&lt;/h2&gt;

&lt;p&gt;I don’t have perfect answers yet, but I know the solution isn’t “more of the same.” We need to actively maintain comprehension as a first-class team property.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Originally published on my blog:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://davidslv.uk/2026/05/17/comprehension-debt.html" rel="noopener noreferrer"&gt;https://davidslv.uk/2026/05/17/comprehension-debt.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What do you think?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Have you seen comprehension debt in your own teams? What (if anything) are you doing about it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>leadership</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Your AI is Only as Good as the System You Give It (Most Engineers Get This Backwards)</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Thu, 07 May 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/davidslv/your-ai-is-only-as-good-as-the-system-behind-it-5efe</link>
      <guid>https://dev.to/davidslv/your-ai-is-only-as-good-as-the-system-behind-it-5efe</guid>
      <description>&lt;p&gt;Every developer I know uses AI the exact same way:&lt;/p&gt;

&lt;p&gt;Open ChatGPT or Claude.&lt;br&gt;&lt;br&gt;
Paste some code.&lt;br&gt;&lt;br&gt;
Explain their architecture from scratch.&lt;br&gt;&lt;br&gt;
Cross their fingers and hope for a useful answer.&lt;/p&gt;

&lt;p&gt;It works… sort of.&lt;br&gt;&lt;br&gt;
Like Googling Stack Overflow in 2015 — you get &lt;em&gt;something&lt;/em&gt;, you tweak it, and you move on.&lt;/p&gt;

&lt;p&gt;But every single time, you start from zero.&lt;/p&gt;

&lt;p&gt;The AI doesn’t know your architecture decisions.&lt;br&gt;&lt;br&gt;
It doesn’t remember the convention your team agreed on last month.&lt;br&gt;&lt;br&gt;
It has no idea you stopped using callbacks for cross-domain side effects.&lt;br&gt;&lt;br&gt;
It doesn’t know &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So you repeat yourself. Every session. Every project. Every day.&lt;/p&gt;

&lt;p&gt;Six months ago I got tired of it.&lt;/p&gt;

&lt;p&gt;I stopped treating AI like a clever junior developer and started treating it like a &lt;strong&gt;senior team member&lt;/strong&gt; — one that actually knows the system.&lt;/p&gt;

&lt;p&gt;Here’s exactly how I built that system.&lt;/p&gt;

&lt;p&gt;Most engineers use AI the same way. Open a chat. Type a question. Get an answer. Repeat.&lt;/p&gt;

&lt;p&gt;The engineers getting disproportionate value from AI aren't better at prompting. They've built a better &lt;strong&gt;system&lt;/strong&gt; around the AI. The difference isn't in how they ask — it's in what the AI already knows before they ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Problem
&lt;/h2&gt;

&lt;p&gt;AI without context produces confident, generic, and often wrong output. &lt;/p&gt;

&lt;p&gt;Ask it to "write a service object" and you’ll get something syntactically correct that violates every convention your team has established. Ask it to "review this pull request" and it gives surface-level feedback that completely misses the architectural implications.&lt;/p&gt;

&lt;p&gt;This isn’t an AI limitation.&lt;br&gt;&lt;br&gt;
It’s a &lt;strong&gt;systems&lt;/strong&gt; problem.&lt;/p&gt;

&lt;p&gt;You wouldn’t expect a new hire to be productive on day one without onboarding, codebase access, and decision context. Yet most engineers give their AI less context than they’d give an intern.&lt;/p&gt;

&lt;p&gt;The fix isn’t better prompts.&lt;br&gt;&lt;br&gt;
The fix is better infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Your Knowledge Machine-Readable
&lt;/h2&gt;

&lt;p&gt;The first step has nothing to do with AI configuration. It’s about organizing your own knowledge so any tool (AI or otherwise) can navigate it.&lt;/p&gt;

&lt;p&gt;I use the &lt;strong&gt;PARA method&lt;/strong&gt; (Projects, Areas, Resources, Archives):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Projects&lt;/strong&gt; — Active work with deadlines
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Areas&lt;/strong&gt; — Ongoing responsibilities
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; — Reference material
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archives&lt;/strong&gt; — Completed work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your knowledge lives in structured directories with consistent naming and Markdown files, an AI agent can actually traverse and understand it. Architectural decisions, coding conventions, and team agreements become readable instead of living in Slack threads or someone’s head.&lt;/p&gt;

&lt;p&gt;The investment isn’t in AI tooling.&lt;br&gt;&lt;br&gt;
It’s in the discipline of writing things down in a format that’s both human and machine-readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the AI Project Context
&lt;/h2&gt;

&lt;p&gt;Once your knowledge is organized, create a short &lt;strong&gt;project context document&lt;/strong&gt; (usually 10–20 lines) for every active project.&lt;/p&gt;

&lt;p&gt;This document acts as an operating manual for the AI. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the project does&lt;/li&gt;
&lt;li&gt;Key architecture decisions&lt;/li&gt;
&lt;li&gt;Important conventions&lt;/li&gt;
&lt;li&gt;Constraints and non-negotiables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of explaining your architecture every time, you just point the AI at this file. The difference is immediate: it stops generating generic code and starts generating code that actually fits your system.&lt;/p&gt;

&lt;p&gt;Ten minutes of writing saves hours of back-and-forth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codifying Your Workflows
&lt;/h2&gt;

&lt;p&gt;Context tells the AI &lt;em&gt;about&lt;/em&gt; your world.&lt;br&gt;&lt;br&gt;
Workflows tell it &lt;em&gt;how you operate&lt;/em&gt; inside that world.&lt;/p&gt;

&lt;p&gt;Write down your repeatable processes in plain Markdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How you run tests&lt;/li&gt;
&lt;li&gt;How you deploy&lt;/li&gt;
&lt;li&gt;How you create pull requests&lt;/li&gt;
&lt;li&gt;How you write commit messages&lt;/li&gt;
&lt;li&gt;How you review code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these workflows are explicit, the AI can follow them exactly — not approximately. Your deployment checklist, code review rules, and architectural guardrails become repeatable.&lt;/p&gt;

&lt;p&gt;This turns implicit senior knowledge into consistent, executable process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting to Your Tools
&lt;/h2&gt;

&lt;p&gt;Stop copy-pasting.&lt;/p&gt;

&lt;p&gt;Modern AI tools can connect directly to your GitHub, monitoring systems, error trackers, and CI pipelines. Every time you find yourself copying data into a chat, that’s a connection you should make.&lt;/p&gt;

&lt;p&gt;The fewer manual steps, the more powerful the AI becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Guardrails
&lt;/h2&gt;

&lt;p&gt;The more autonomy you give AI, the more important boundaries become.&lt;/p&gt;

&lt;p&gt;Define hard rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never commit credential files&lt;/li&gt;
&lt;li&gt;Never run destructive database commands without confirmation&lt;/li&gt;
&lt;li&gt;Never force-push to main&lt;/li&gt;
&lt;li&gt;Respect engine boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These guardrails let you give the AI more freedom safely — the same way good CI/CD pipelines protect production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Principle
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI amplifies whatever system you give it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give it no system and it amplifies chaos — confident, well-formatted chaos.&lt;/p&gt;

&lt;p&gt;Give it structured knowledge, persistent context, codified workflows, tool connections, and clear guardrails, and it becomes one of the most valuable colleagues you have.&lt;/p&gt;

&lt;p&gt;The engineers who will get the most out of AI in the coming years won’t be the best prompt engineers.&lt;/p&gt;

&lt;p&gt;They’ll be the best &lt;strong&gt;systems&lt;/strong&gt; engineers.&lt;/p&gt;




&lt;p&gt;If you want a starting point, I’ve open-sourced my own template based on the PARA method:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/Davidslv/ben-ai-system" rel="noopener noreferrer"&gt;ben-ai-system&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What’s one piece of context you wish your AI already knew about your projects? Drop it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The Tailwind Debate Isn't About CSS — It's About Abstraction Layers (And Why Most People Get It Wrong)</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Wed, 06 May 2026 19:44:15 +0000</pubDate>
      <link>https://dev.to/davidslv/the-tailwind-debate-is-really-about-abstraction-470c</link>
      <guid>https://dev.to/davidslv/the-tailwind-debate-is-really-about-abstraction-470c</guid>
      <description>&lt;p&gt;Two posts appeared on Dev.to a few days ago. One titled &lt;a href="https://dev.to/sylwia-lask/i-love-tailwind-sorry-not-sorry-5cfh"&gt;"I Love Tailwind — Sorry Not Sorry."&lt;/a&gt; The other: &lt;a href="https://dev.to/freshcaffeine/i-dont-like-tailwind-sorry-not-sorry-50b5"&gt;"I Don't Like Tailwind — Sorry Not Sorry."&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same format. Opposite conclusions. Hundreds of comments. The community tearing itself apart over utility classes.&lt;/p&gt;

&lt;p&gt;I've seen this fight before. So have you.&lt;/p&gt;

&lt;p&gt;Microservices vs monoliths. Raw SQL vs ORMs. REST vs GraphQL. Vim vs Emacs if you've been around long enough. The technology changes. The argument doesn't.&lt;/p&gt;

&lt;p&gt;It's always about the same thing: should you use primitives directly, or build an abstraction on top of them?&lt;/p&gt;

&lt;p&gt;Tailwind gives you utility classes. Small, single-purpose, composable. That's the primitive layer. And at that level, it's genuinely well-designed. The design tokens are consistent. The naming conventions make sense. The build system strips what you don't use.&lt;/p&gt;

&lt;p&gt;The problem isn't Tailwind. The problem is using the primitive layer as your application layer.&lt;/p&gt;

&lt;p&gt;When your HTML looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 px-6 py-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded-lg shadow-md transition-colors duration-200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;example&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You haven't eliminated CSS complexity. You've moved it. It's the same information, scattered across templates instead of collected in stylesheets. At scale, those class strings become just as unreadable as the CSS they replaced. Arguably worse — because at least CSS has selectors that name things.&lt;/p&gt;

&lt;p&gt;The teams that love Tailwind? They're not writing those class strings everywhere. They're using it as a foundation for component libraries. DaisyUI. Shadcn. Headless UI. They write the utility classes once, inside a component, and the rest of the application uses the component. The abstraction layer does the work.&lt;/p&gt;

&lt;p&gt;This is the same principle as software architecture. You don't scatter database queries across your controllers. You build a model layer. You don't scatter HTTP calls across your views. You build a service layer. The primitive exists so you can build on top of it, not so you can use it directly in every file.&lt;/p&gt;

&lt;p&gt;Microservices vs monoliths is the same argument at a different scale. Microservices are the primitives — small, single-purpose, independently deployable. Powerful at the infrastructure level. But if every feature requires coordinating five services, you haven't reduced complexity. You've distributed it. The teams that succeed with microservices are the ones that build the right abstraction layer on top: API gateways, service meshes, shared libraries, or — more often than the industry admits — they merge services back into a modular monolith and keep the boundaries without the network calls.&lt;/p&gt;

&lt;p&gt;The pattern repeats because the underlying tension is real. Primitives give you power and flexibility. Abstractions give you readability and maintainability. You need both. The question is where the boundary sits.&lt;/p&gt;

&lt;p&gt;Use Tailwind? Fine. Build components on top of it. Use microservices? Fine. Make sure the abstraction layer justifies the distribution cost. Use raw SQL? Fine. Wrap it in something that names what it does.&lt;/p&gt;

&lt;p&gt;The tool is never the problem. The problem is mistaking the primitive layer for the finished architecture.&lt;/p&gt;

&lt;p&gt;Every few years we have this argument again, wearing different clothes. The answer is always the same: build on top of your primitives. Don't scatter them everywhere.&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>discuss</category>
      <category>architecture</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Stop Rewriting Your Ruby on Rails Monolith as Microservices — Use Engines Instead (Here's How)</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Wed, 06 May 2026 12:54:31 +0000</pubDate>
      <link>https://dev.to/davidslv/how-rails-engines-can-isolate-your-monolith-without-microservices-58eo</link>
      <guid>https://dev.to/davidslv/how-rails-engines-can-isolate-your-monolith-without-microservices-58eo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is an adapted excerpt from Chapter 1 of &lt;a href="https://davidslv.uk/modular-rails/" rel="noopener noreferrer"&gt;Modular Rails: Architecture for the Long Game&lt;/a&gt;, my book on building maintainable Ruby on Rails applications using Rails Engines.&lt;/em&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The goal of software architecture is to minimize the human resources required to build and maintain the required system."&lt;/em&gt;&lt;br&gt;
-- Robert C. Martin, &lt;em&gt;Clean Architecture&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Cost of Change Over Time
&lt;/h2&gt;

&lt;p&gt;Every Software Engineer that uses Ruby on Rails has lived this story. The application starts small. A handful of models, a few controllers, a test suite that runs in seconds. Adding a feature is straightforward -- you create a model, write a migration, build a controller, add some views. The framework guides you. Convention over configuration. Life is good.&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%2Frgnudjhidlujaum4fjul.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%2Frgnudjhidlujaum4fjul.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the application grows. The &lt;code&gt;app/models&lt;/code&gt; directory fills up. The &lt;code&gt;User&lt;/code&gt; model gains associations to everything. Service objects proliferate in &lt;code&gt;app/services/&lt;/code&gt;. Someone introduces an &lt;code&gt;app/interactors/&lt;/code&gt; directory. Then &lt;code&gt;app/queries/&lt;/code&gt;. Then &lt;code&gt;app/decorators/&lt;/code&gt;. Each new directory is a well-intentioned attempt to manage complexity, but none of them create actual boundaries. Everything can still reference everything.&lt;/p&gt;

&lt;p&gt;At this point, the cost of change starts to climb. Not linearly -- exponentially. A small change to the billing logic triggers test failures in the notification suite. A database migration for user preferences locks a table that the checkout flow depends on.&lt;/p&gt;

&lt;p&gt;Let's make this concrete. You add a discount field to invoices -- a straightforward billing change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bin/rails &lt;span class="nb"&gt;test test&lt;/span&gt;/models/billing/invoice_test.rb
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;3 tests, 3 assertions, 0 failures
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bin/rails &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;847 tests, 1203 assertions, 4 failures
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Failures &lt;span class="k"&gt;in&lt;/span&gt;: notification_mailer_test.rb, report_generator_test.rb,
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;admin_dashboard_test.rb, webhook_handler_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four failures in four files that have nothing to do with discounts. None of these files are in the &lt;code&gt;billing/&lt;/code&gt; directory. None of them showed up when you grepped for the code you changed. But they all reached into the invoice model directly, without going through any kind of boundary.&lt;/p&gt;

&lt;p&gt;Now imagine the same change in a codebase where billing lives inside an engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;engines/billing &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;94 examples, 0 failures
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../.. &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;312 examples, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero collateral damage. The engine's boundary means billing tests only load billing code. If the billing tests pass, the change is safe.&lt;/p&gt;

&lt;p&gt;This is not a Rails problem. This is an architecture problem. Or more precisely, it's the absence of architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conway's Law and Team Structure
&lt;/h2&gt;

&lt;p&gt;In 1968, Melvin Conway observed that &lt;em&gt;"any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure."&lt;/em&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%2Fb1b1iapb448y9ea26vdh.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%2Fb1b1iapb448y9ea26vdh.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your team is structured as a single unit working on a single codebase with no internal boundaries, the application will reflect that: a single, undifferentiated mass where everything knows about everything.&lt;/p&gt;

&lt;p&gt;But Conway's Law also works in reverse -- the "Inverse Conway Manoeuvre" (coined by Jonny LeRoy and Matt Simons in 2010, and later popularised by James Lewis and Martin Fowler). If you structure your codebase into well-bounded modules, each with a clear domain and interface, you create natural team boundaries. The billing engine has an owner. The notification engine has an owner. Changes to billing don't require coordination with the notification team because the engine boundary makes the coupling explicit and manageable.&lt;/p&gt;

&lt;p&gt;In a Rails context, this means that your &lt;code&gt;app/&lt;/code&gt; directory structure isn't just a filing system. It's an organisational decision. And &lt;code&gt;app/models/&lt;/code&gt; with 200 files in it is an organisational decision that says "everyone works on everything, and good luck coordinating."&lt;/p&gt;

&lt;h2&gt;
  
  
  Deferring Decisions
&lt;/h2&gt;

&lt;p&gt;Perhaps the most counterintuitive idea in software architecture comes from Robert C. Martin:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A good architect pretends that the decision has not been made, and shapes the system such that those decisions can still be deferred or changed for as long as possible."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what a deferred decision looks like in code. Your billing engine needs a payment gateway, but the right choice depends on which markets you'll launch in -- information you don't have yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# engines/billing/lib/billing.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Billing&lt;/span&gt;
  &lt;span class="n"&gt;mattr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:payment_gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"Billing::Gateways::Stripe"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business logic calls &lt;code&gt;Billing.payment_gateway.constantize.new&lt;/code&gt; and never mentions Stripe, Adyen, or anyone else by name. When the business decides to expand into a market where Stripe isn't available, you write a new gateway class and change one line of configuration. No billing logic changes. No tests break.&lt;/p&gt;

&lt;p&gt;That boundary is itself a deferred decision. By keeping billing isolated in an engine, you've deferred the decision about whether billing should be a separate service, a separate application, or remain part of the monolith. You can make that decision later, with more information, at lower cost.&lt;/p&gt;

&lt;p&gt;This is the essence of good architecture: not making the perfect decision now, but structuring the system so that you can make the right decision later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This was Chapter 1 of &lt;a href="https://davidslv.uk/modular-rails/" rel="noopener noreferrer"&gt;Modular Rails: Architecture for the Long Game&lt;/a&gt;. The book covers 18 chapters across four parts -- from Clean Architecture principles to extracting your first engine, testing strategies, team workflow, and the honest trade-offs most architecture books skip.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://www.amazon.co.uk/dp/B0GZL7D53M" rel="noopener noreferrer"&gt;Get the book on Amazon UK&lt;/a&gt; · &lt;a href="https://www.amazon.com/dp/B0GZL7D53M" rel="noopener noreferrer"&gt;Amazon US&lt;/a&gt; · &lt;a href="https://davidslv.uk/modular-rails/" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>softwareengineering</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I Built a Dungeon Crawler Game in Ruby (And It Actually Works)</title>
      <dc:creator>David Silva</dc:creator>
      <pubDate>Sat, 22 Nov 2025 11:24:00 +0000</pubDate>
      <link>https://dev.to/davidslv/i-built-a-dungeon-crawler-game-in-ruby-and-it-actually-works-40lf</link>
      <guid>https://dev.to/davidslv/i-built-a-dungeon-crawler-game-in-ruby-and-it-actually-works-40lf</guid>
      <description>&lt;p&gt;When I tell people I'm building a game in Ruby, I get &lt;em&gt;looks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;"Ruby? For a game? Isn't that... slow?"&lt;/p&gt;

&lt;p&gt;Fair question. Most game developers reach for C++, C#, or Unity. Ruby is for web apps, not games. Everyone knows this.&lt;/p&gt;

&lt;p&gt;Except I've been building a roguelike – a procedurally generated dungeon crawler inspired by the 1980s classic &lt;em&gt;Rogue&lt;/em&gt; – in Ruby for six months now, and it's been brilliant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Building
&lt;/h2&gt;

&lt;p&gt;Think classic dungeon crawler: ASCII graphics, procedurally generated mazes, turn-based movement, permadeath. Your character (&lt;code&gt;@&lt;/code&gt;) navigates randomly generated dungeons, fighting monsters, collecting items, trying not to die.&lt;/p&gt;

&lt;p&gt;It runs entirely in the terminal. No fancy graphics. Just pure game logic and procedural generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unconventional Choice
&lt;/h2&gt;

&lt;p&gt;I'm not a game developer by training. I build web applications and APIs. So choosing Ruby for a game felt both natural and slightly mad.&lt;/p&gt;

&lt;p&gt;But here's the thing: I wasn't building a AAA title. I was building a turn-based game that runs in the terminal. My bottleneck wasn't performance. It was understanding game architecture.&lt;/p&gt;

&lt;p&gt;For that, Ruby's clarity was perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rapid Iteration Changed Everything
&lt;/h2&gt;

&lt;p&gt;No compilation step. No waiting. Just change the code and run it.&lt;/p&gt;

&lt;p&gt;I implemented four different maze generation algorithms: Binary Tree, Aldous-Broder, Recursive Backtracker, and Recursive Division. Here's how simple the Binary Tree algorithm looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BinaryTree&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;AbstractAlgorithm&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_cell&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;has_north&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="n"&gt;has_east&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_north&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;has_east&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;has_north&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;north&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;has_east&lt;/span&gt;
        &lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cell: &lt;/span&gt;&lt;span class="n"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;bidirectional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;grid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that. &lt;code&gt;grid.each_cell&lt;/code&gt; reads like English. The logic is clear: randomly connect each cell either north or east. I could tweak this, run it, see results immediately. No fuss.&lt;/p&gt;

&lt;p&gt;When you're learning game architecture – which I was – this feedback loop is invaluable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components Stay Simple
&lt;/h2&gt;

&lt;p&gt;The core of my game uses Entity-Component-System (ECS) architecture. Components should be pure data containers, and Ruby makes this trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PositionComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Component&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="vi"&gt;@row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="vi"&gt;@column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
    &lt;span class="vi"&gt;@column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's my entire &lt;code&gt;PositionComponent&lt;/code&gt;. No boilerplate. No getters and setters cluttering the code. Ruby's &lt;code&gt;attr_reader&lt;/code&gt; handles it. Named parameters make it obvious what you're passing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PositionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;row: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to languages where you need builder patterns just to maintain readability. Ruby gets out of your way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Performance?
&lt;/h2&gt;

&lt;p&gt;Let's be honest: Ruby isn't fast.&lt;/p&gt;

&lt;p&gt;For my use case? Didn't matter. The game is turn-based. It waits for player input. The bottleneck is human reaction time, not Ruby's execution speed.&lt;/p&gt;

&lt;p&gt;Maze generation on an 80×40 grid? Milliseconds. Entity queries with dozens of entities? Trivial. If I were building a real-time action game with hundreds of entities updating 60 times per second, Ruby would be the wrong choice.&lt;/p&gt;

&lt;p&gt;But I wasn't. Context matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Joy Matters Too
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect: Ruby made me &lt;em&gt;happy&lt;/em&gt; while coding.&lt;/p&gt;

&lt;p&gt;When I write &lt;code&gt;entity.has_component?(:position)&lt;/code&gt;, it reads like a question I'd ask a colleague. The code communicates intent clearly. I could focus on understanding ECS, event systems, and procedural generation rather than fighting with syntax.&lt;/p&gt;

&lt;p&gt;When you're building something complex in your spare time, enjoyment matters. If I'd chosen a language I found frustrating, I might have abandoned the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The game now has procedurally generated mazes, an Entity-Component-System architecture, event-driven logging, command-based input handling, and 48 spec files of test coverage.&lt;/p&gt;

&lt;p&gt;Not bad for a language supposedly "not meant for games."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Want the full story?&lt;/strong&gt; I wrote a comprehensive article on my blog covering the complete architecture, testing strategy, and lessons learned: &lt;a href="https://davidslv.uk/ruby/game-development/2025/11/22/why-ruby-roguelike.html" rel="noopener noreferrer"&gt;Why I Chose Ruby to Build a Roguelike Game&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is &lt;a href="https://github.com/Davidslv/vanilla-roguelike" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt; if you want to see how it all fits together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; – I documented this entire journey in a book: &lt;a href="https://www.amazon.com/Building-Your-Own-Roguelike-Hands-ebook/dp/B0G1RBWF6V" rel="noopener noreferrer"&gt;Building Your Own Roguelike: A Practical Guide&lt;/a&gt;. It walks through building this from scratch – the ECS pattern, event systems, maze generation algorithms, and everything you see in Vanilla Roguelike.&lt;/p&gt;

&lt;p&gt;Thank you for reading,&lt;br&gt;
David Silva&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>gamedev</category>
      <category>roguelike</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
