<?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: ThatGhost</title>
    <description>The latest articles on DEV Community by ThatGhost (@thatghost).</description>
    <link>https://dev.to/thatghost</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%2F3843129%2Ffeb103f0-3fbc-4bf0-ae02-7dce96020a73.jpg</url>
      <title>DEV Community: ThatGhost</title>
      <link>https://dev.to/thatghost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thatghost"/>
    <language>en</language>
    <item>
      <title>Result is not error handling</title>
      <dc:creator>ThatGhost</dc:creator>
      <pubDate>Sun, 12 Apr 2026 19:14:58 +0000</pubDate>
      <link>https://dev.to/thatghost/result-is-not-error-handling-13md</link>
      <guid>https://dev.to/thatghost/result-is-not-error-handling-13md</guid>
      <description>&lt;p&gt;We talk about the Result pattern like it's a better way to handle errors. Return a Result instead of throwing an exception, wrap your failure cases, done. That framing is not wrong, but it undersells what Result actually is.&lt;/p&gt;

&lt;p&gt;Result is a flow control tool. Error handling is just a side effect.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with raw returns and exceptions
&lt;/h2&gt;

&lt;p&gt;When a service returns a raw value or throws on failure, the caller owns the failure logic. Every caller. That means domain knowledge — what "not found" means, what "invalid state" means — leaks upward into code that shouldn't care about it.&lt;/p&gt;

&lt;p&gt;Exceptions make this worse because they're invisible in the signature. Nothing in this tells you what can go wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have to read the implementation, or get surprised at runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Result as a communication contract
&lt;/h2&gt;

&lt;p&gt;The shift is treating success and failure as equally valid outcomes, both visible in the signature, both something the caller can reason about without reading the implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DeleteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&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;You already know what this service can tell you. You don't need to hunt for thrown exceptions or check for nulls. The contract is in the signature.&lt;/p&gt;




&lt;h2&gt;
  
  
  The chain
&lt;/h2&gt;

&lt;p&gt;Where Result earns its keep is in composition. Once your services all speak the same language, you can pipeline them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AnonymizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&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="n"&gt;_userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_anonymizationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnonymizeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendConfirmationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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;Three services. No if/else. No try/catch. If any step fails, the failure propagates, and the rest of the chain doesn't execute. The happy path and the failure path are both readable in the same six lines.&lt;/p&gt;

&lt;p&gt;This is the thing most explanations of Result miss. It's not just cleaner error handling. It's a way to express a multi-step flow as a single readable unit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The controversial claim
&lt;/h2&gt;

&lt;p&gt;Exceptions should be for things you didn't plan for. Infrastructure failures. Bugs. Things that represent a broken assumption about the world.&lt;/p&gt;

&lt;p&gt;If/else branching in service code is a smell. It means the caller is making decisions that belong to the callee, or that flow logic is scattered across layers instead of being composed in one place.&lt;/p&gt;

&lt;p&gt;Result is the third option. And once you start using it consistently, it should be your default return type for anything that can meaningfully fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure is composable too
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Because failure is a first-class value, you can do logic on it.&lt;/p&gt;

&lt;p&gt;In a transactional flow, a failure mid-chain isn't just something to report — it's something to act on. You can inspect the error, trigger compensating logic, revert what's already happened, and still return a clean Result to the caller. The failure path is as composable as the success path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_paymentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChargeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_inventoryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsFailure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_paymentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RefundAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&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;No exception handler. No special casing. Just a Result you can inspect and act on like any other value.&lt;/p&gt;




&lt;h2&gt;
  
  
  The connective tissue
&lt;/h2&gt;

&lt;p&gt;In the previous posts in this series we established what your classes are — Services, Providers, Mutators — and how they relate through DI rather than inheritance. Result is what makes that system actually work at runtime.&lt;/p&gt;

&lt;p&gt;Without a shared communication contract, a flat graph of injected services is just a naming convention. With Result, every class in the graph speaks the same language. Flow is explicit. Failure is visible. And the whole thing stays readable without exceptions leaking through layers or if/else logic colonizing your service code.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The AI Should Touch the Layout, Let Your Endpoints Do the Rest</title>
      <dc:creator>ThatGhost</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:39:47 +0000</pubDate>
      <link>https://dev.to/thatghost/the-ai-should-touch-the-layout-let-your-endpoints-do-the-rest-23ec</link>
      <guid>https://dev.to/thatghost/the-ai-should-touch-the-layout-let-your-endpoints-do-the-rest-23ec</guid>
      <description>&lt;p&gt;We've been building a dashboarding app at work. It looks great. The design is solid, the components are polished, and every time we show it to a stakeholder they love it.&lt;/p&gt;

&lt;p&gt;It also takes forever to wire up.&lt;/p&gt;

&lt;p&gt;Every new view is the same cycle: a stakeholder wants to see something, a developer builds an endpoint, another developer connects it to a component, someone tweaks the layout, and three days later the stakeholder sees the thing they asked for a week ago. Multiply that by every team, every quarter, every "can we just add one more metric" request, and you start to feel like you're running a custom dashboard factory instead of building a product.&lt;/p&gt;

&lt;p&gt;We knew the components were good. The problem was the wiring.&lt;/p&gt;




&lt;h2&gt;
  
  
  The obvious AI answer is wrong
&lt;/h2&gt;

&lt;p&gt;The first instinct is to let the AI query your database. Natural language to SQL, job done. Except it isn't.&lt;/p&gt;

&lt;p&gt;You get hallucinated joins, filters that look right but produce wrong aggregates, and a model that has been handed direct access to your production data with nothing structural stopping it from being manipulated by whatever it reads along the way. And when something goes wrong — when a number is off, when a report looks plausible but isn't — you have no audit trail. Every query was generated fresh. Nothing is stable or reviewable.&lt;/p&gt;

&lt;p&gt;The deeper problem is that this gives the AI the wrong job. The AI is good at understanding intent and composing structure. It is not a trustworthy query engine, and treating it like one trades one wiring problem for a much worse one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight: the AI should touch the layout, not the data
&lt;/h2&gt;

&lt;p&gt;What if the AI never saw your data at all?&lt;/p&gt;

&lt;p&gt;Instead of asking the AI to fetch data, you ask it to decide what to show and where to show it — and then let your existing, validated endpoints do the actual fetching. The AI produces a layout manifest. The client executes it.&lt;/p&gt;

&lt;p&gt;The flow looks like this. When a user submits a query, a vector search runs against a store of pre-registered endpoint descriptions and retrieves the semantically relevant subset. That subset, combined with a static component registry, gets injected into the AI context. The AI reasons about intent, selects components, computes any derived values like date ranges, assigns a bento-grid layout, and outputs a JSON document — the UI Manifest. No data flows through the AI at any point. It only ever sees descriptions of what endpoints exist, not what they return.&lt;/p&gt;

&lt;p&gt;The client receives the manifest, renders the component shells, and each component independently fetches its own data from the URL the AI specified. Standard HTTP calls, standard backend validation, standard response handling. The data pipeline is completely unchanged. The AI just decided what to ask for and where to put it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The registry is the trust boundary
&lt;/h2&gt;

&lt;p&gt;The component registry is a static list the development team maintains. Each entry defines a component name, the data type it accepts — one of &lt;code&gt;Metric&lt;/code&gt;, &lt;code&gt;Dataset&lt;/code&gt;, or &lt;code&gt;List&lt;/code&gt; — and the minimum column span it requires in the 12-column grid. The AI can only reference components that exist in that list. It cannot invent a component. It cannot reference an endpoint that has not been registered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"component_registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LineChart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accepts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dataset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minCol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StatCard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"accepts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Metric"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"minCol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DataTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accepts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"List"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"minCol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an intentional bottleneck. Every component in the registry has been built and tested by the team. Every endpoint has been written, validated, and registered with a parameter schema. The backend validates all parameters on every request before executing anything — the AI constructs a URL, the backend decides whether it is valid, and the data only moves if it passes that gate.&lt;/p&gt;

&lt;p&gt;What you end up with is a system where the AI operates inside a box your team defines. Prompt injection in a user query cannot cause the AI to call an unregistered endpoint — it is not in context and would fail backend validation if constructed. The AI cannot see raw table names, schema details, or connection strings. There is nothing to exfiltrate during the generation phase because there is no data in the generation phase.&lt;/p&gt;

&lt;p&gt;The registry is not just a config file. It is the security model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this actually produces
&lt;/h2&gt;

&lt;p&gt;For a query like "show me sales versus targets from last week", the AI calculates the date range, selects a &lt;code&gt;LineChart&lt;/code&gt; for the trend and a &lt;code&gt;StatCard&lt;/code&gt; for the total, assigns the chart most of the grid width because it is the primary insight, and outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dashboardTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Weekly Performance Overview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StatCard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gridSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"colSpan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rowSpan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Total Revenue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dataUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/sales/total-revenue?startDate=2026-03-30&amp;amp;endDate=2026-04-05"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LineChart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gridSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"colSpan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rowSpan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sales vs Target Trend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dataUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/sales/weekly-trend?startDate=2026-03-30&amp;amp;endDate=2026-04-05"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client renders two component shells. Each fetches its own data independently. The dashboard appears. The AI never saw a single revenue figure — it just decided where to put the boxes and which endpoints to point them at.&lt;/p&gt;

&lt;p&gt;Each endpoint returns data in one of the three standardized payload types. A &lt;code&gt;Metric&lt;/code&gt; is a scalar value with an optional delta. A &lt;code&gt;Dataset&lt;/code&gt; is columnar data with named columns and rows. A &lt;code&gt;List&lt;/code&gt; is a collection of typed records. Components know how to render their payload type. The manifest connects them. That standardization is also what makes the whole thing framework-agnostic — Blazor, React, Angular, and Vue all just need to implement the same three renderers and a manifest parser. The AI logic and the endpoint layer are completely shared.&lt;/p&gt;




&lt;h2&gt;
  
  
  The side effect: user-generated dashboards for free
&lt;/h2&gt;

&lt;p&gt;Once you have a manifest, you have a JSON blob. And a JSON blob can be saved.&lt;/p&gt;

&lt;p&gt;When a user likes what the AI built, they save the manifest. It becomes a named, persistent dashboard — no SQL knowledge required, no developer involved, no custom view built on request. The saved artifact is a pointer to endpoints your team already validated, with a layout attached. It is versionable, shareable, and auditable in the same way any other application data is. It is not an opaque generated query sitting in someone's personal schema.&lt;/p&gt;

&lt;p&gt;This is what changes the relationship between business users and data-heavy software. The gap has never been the data. The gap has been the translation layer between "I want to know X" and "here is X, rendered clearly." That translation has historically required a developer. This pattern removes that requirement without removing developer control over what is expressible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The immediate application is dashboards, but the pattern is general. Any application with a curated component library and a set of validated endpoints can adopt it. The AI becomes a translator between user intent and application capability, bounded by whatever the development team chose to expose.&lt;/p&gt;

&lt;p&gt;And at the broadest level, this is what AI in enterprise software should look like. Not an autonomous agent with broad data access, but an orchestration layer that makes existing, validated capabilities accessible to anyone who can ask a question. The organization controls what is inside the box. The AI makes the box useful to everyone.&lt;/p&gt;

&lt;p&gt;We did not set out to design a pattern. We just wanted to stop spending three days on every dashboard request. But the solution we landed on turns out to generalize cleanly, and it solves a problem a lot of teams are going to run into as they try to add AI to software that handles real data.&lt;/p&gt;

&lt;p&gt;The AI should not talk to your data. It should talk to your layout. Let your endpoints do the rest.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>There Is No Such Thing As a Service</title>
      <dc:creator>ThatGhost</dc:creator>
      <pubDate>Wed, 01 Apr 2026 08:36:36 +0000</pubDate>
      <link>https://dev.to/thatghost/there-is-no-such-thing-as-a-service-1mdc</link>
      <guid>https://dev.to/thatghost/there-is-no-such-thing-as-a-service-1mdc</guid>
      <description>&lt;p&gt;If you have been following this series, you know I am a fan of services. Dependency injection, single responsibility, clean boundaries between concerns. Done right, you end up with hundreds of services, each doing exactly one thing.&lt;/p&gt;

&lt;p&gt;But here is the problem nobody talks about: the word "service" does not actually mean anything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ItemService&lt;/code&gt;. What does it do? Everything. What is inside? Who knows. You have to open it and start reading. And the more your codebase grows, the more that class becomes a dumping ground, a god class disguised by a reasonable name.&lt;/p&gt;

&lt;p&gt;I want to argue that the service as we know it is just one of many distinct types of classes we could be writing. And the moment you start thinking in terms of those types, your code becomes something anyone can navigate on instinct alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With "Service"
&lt;/h2&gt;

&lt;p&gt;When a codebase is healthy, your services are focused. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Users
  UserService
  AuthenticationService
  AuthorizationService
  TokenService

/Items
  ItemService
  ItemRepository
  ItemCartService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks great. But zoom in on &lt;code&gt;ItemService&lt;/code&gt; six months later and you will likely find something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;GetItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;GetItemsByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetItemsByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetLatestItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;DeleteItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;DeleteItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;InsertItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;InsertMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;UpsertItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a service. That is a warehouse. It has read logic, write logic, bulk operations, filtering strategies, all crammed under one roof because "it is about items, so it goes in &lt;code&gt;ItemService&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;The class is technically focused on one entity. But it is not focused on one responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rethinking the Taxonomy
&lt;/h2&gt;

&lt;p&gt;Here is the shift I am proposing: stop thinking of a service as a place where you house logic. Start thinking of it as a datatype.&lt;/p&gt;

&lt;p&gt;When you see &lt;code&gt;int&lt;/code&gt;, you do not need documentation. You know it is a whole number. You know what operations make sense on it. The name carries the contract. We should hold our classes to the same standard. When you see &lt;code&gt;ItemProvider&lt;/code&gt;, you should know just as instinctively what it contains and what it does, without opening the file, without reading a single method signature.&lt;/p&gt;

&lt;p&gt;Instead of having one service per entity, you define services by what they &lt;em&gt;do&lt;/em&gt;. The name of the class should tell you exactly what is inside before you ever open the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Service
&lt;/h3&gt;

&lt;p&gt;The classic. It still has its place, especially for simpler entities or flows that do not warrant a full split. A proper service handles coordinated logic for an entity, but keeps its surface area small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;GetItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;InsertItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;DeleteItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&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;Simple, stable, predictable. If it starts growing, that is your signal to promote some of its responsibilities into more specialized types.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Provider
&lt;/h3&gt;

&lt;p&gt;A Provider is a read-only class. Every method on it is a query. Nothing writes, nothing mutates. If you need data about an entity, you go to its Provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;GetOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetMany&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;GetByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;GetFeatured&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;You do not need to read the method bodies. You do not need to wonder whether &lt;code&gt;GetByName&lt;/code&gt; has a side effect somewhere. It is a Provider. It gets things. That is the entire contract.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Mutator
&lt;/h3&gt;

&lt;p&gt;The Mutator is the other half of that split. It owns all write operations. Inserts, updates, deletes, upserts. If data is changing, it goes through the Mutator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ItemMutator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;InsertMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;UpdateMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;Upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;UpsertMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;DeleteMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;ids&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;Notice what this does to readability. If you see an &lt;code&gt;ItemMutator&lt;/code&gt; being injected somewhere, you immediately know that code is writing data. If you see an &lt;code&gt;ItemProvider&lt;/code&gt;, you know it is only reading. Your call sites become self-documenting.&lt;/p&gt;




&lt;h2&gt;
  
  
  You Already Do This
&lt;/h2&gt;

&lt;p&gt;This is not a new concept. You already apply it to specialized cases without thinking about it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JobHandler&lt;/code&gt;. &lt;code&gt;EmailSender&lt;/code&gt;. &lt;code&gt;PaymentProcessor&lt;/code&gt;. You already recognized those as distinct enough to deserve their own naming convention. You did not call them &lt;code&gt;JobService&lt;/code&gt; or &lt;code&gt;EmailService&lt;/code&gt; because the name would have been too vague.&lt;/p&gt;

&lt;p&gt;The argument is simple: apply that same instinct to the general stuff too.&lt;/p&gt;

&lt;p&gt;A few other types that naturally emerge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifiers&lt;/strong&gt; handle validation and state checks. Does this entity exist? Is this transition allowed? Is the user eligible?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformers&lt;/strong&gt; map one object to another. DTOs to domain models, API responses to internal types, one version of a POCO to another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You probably already have logic like this scattered across your services. Pulling it into named types makes it findable and reusable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The benefit is not just organization. It is communication.&lt;/p&gt;

&lt;p&gt;When a new developer joins and sees &lt;code&gt;ItemProvider&lt;/code&gt;, they know where to look for queries. When you are reviewing a pull request and see an &lt;code&gt;ItemMutator&lt;/code&gt; injected into a controller, you immediately know that endpoint writes data. You did not open a file. You did not read a method signature. The name told you.&lt;/p&gt;

&lt;p&gt;That is the standard we already hold primitive types to. Nobody reads the documentation for &lt;code&gt;bool&lt;/code&gt;. Nobody opens the definition of &lt;code&gt;int&lt;/code&gt; to understand what operations make sense on it. The name carries the contract, and the contract is never violated.&lt;/p&gt;

&lt;p&gt;Your service layer can work the same way. Name the type, keep the class honest to that name, and the codebase stops being something people navigate and starts being something people read.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>If you have a DI container, you don't need inheritance</title>
      <dc:creator>ThatGhost</dc:creator>
      <pubDate>Sat, 28 Mar 2026 19:03:04 +0000</pubDate>
      <link>https://dev.to/thatghost/if-you-have-a-di-container-you-dont-need-inheritance-25lp</link>
      <guid>https://dev.to/thatghost/if-you-have-a-di-container-you-dont-need-inheritance-25lp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 2 of a series of unconventional programming opinions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one tends to get a reaction. So let me be clear about what I'm actually saying before you close the tab.&lt;/p&gt;

&lt;p&gt;I'm not saying inheritance is always wrong. I'm saying that if your project already uses a dependency injection container — and most modern backend projects do — then you're already holding every tool you need to do what inheritance does. Using both at the same time doesn't give you more power. It gives you two competing mental models pulling the codebase in different directions.&lt;/p&gt;

&lt;p&gt;Pick one. And if DI is already in the project, let it do the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What inheritance is actually promising you
&lt;/h2&gt;

&lt;p&gt;Inheritance shows up in codebases for two main reasons: code reuse and polymorphic behaviour. A base class that all your repositories extend so they share CRUD logic. A base handler that all your request handlers extend so they share validation. Abstract methods that force subclasses to implement specific behaviour.&lt;/p&gt;

&lt;p&gt;These are real problems worth solving. But inheritance isn't the only way to solve them — and in a DI heavy project, it's usually not the cleanest way either.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reuse: just use a service
&lt;/h2&gt;

&lt;p&gt;The most common argument for inheritance is reuse. You have shared logic — database calls, logging, mapping — and you want it available everywhere without copy pasting.&lt;/p&gt;

&lt;p&gt;But that's exactly what services are for. Instead of a &lt;code&gt;BaseRepository&amp;lt;T&amp;gt;&lt;/code&gt; that everything inherits from, you write a &lt;code&gt;QueryService&lt;/code&gt; (or a &lt;code&gt;DataAccessService&lt;/code&gt;, or whatever fits your domain) that exposes the shared logic as methods, and you inject it wherever it's needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of this:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&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;span class="c1"&gt;// Do this:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IQueryService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;The logic lives in one place. It's testable in isolation. Any service that needs it just asks for it. And you haven't introduced a class hierarchy that locks your types into a rigid vertical structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polymorphism: that's what interfaces are for
&lt;/h2&gt;

&lt;p&gt;The second argument is behaviour — specifically, the ability to swap one implementation for another at runtime or test time. A &lt;code&gt;PaymentProcessor&lt;/code&gt; that handles Stripe differently than PayPal. A &lt;code&gt;NotificationSender&lt;/code&gt; that emails in production and does nothing in tests.&lt;/p&gt;

&lt;p&gt;Interfaces handle this cleanly. You define the contract, and each implementation stands on its own. No shared base class, no fragile inheritance chain where changing the parent breaks six children you didn't expect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPaymentProcessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaymentResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripeProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPaymentProcessor&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PayPalProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IPaymentProcessor&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;Your DI container decides which one gets injected. The calling code never knows which implementation it's talking to. That's the polymorphism promise — and you got it without touching &lt;code&gt;abstract&lt;/code&gt; or &lt;code&gt;override&lt;/code&gt; once.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hidden cost: base classes need dependencies too
&lt;/h2&gt;

&lt;p&gt;Here's a concrete problem that doesn't show up until a project grows. Base classes aren't isolated — they often need services of their own. And when they do, every single subclass has to carry those dependencies through its constructor, even if it never directly uses them.&lt;/p&gt;

&lt;p&gt;Say you have a &lt;code&gt;BaseService&lt;/code&gt; that handles logging and auditing. You start with one dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&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="c1"&gt;// ... 17 more&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manageable. Then six months later you need to add auditing to the base class. &lt;code&gt;BaseService&lt;/code&gt; now needs an &lt;code&gt;IAuditService&lt;/code&gt;. So you update the base constructor — and now you have to touch every single subclass to thread that new dependency through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You change this:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&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="c1"&gt;// And now you're doing this across 20 files:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShipmentService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAuditService&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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="c1"&gt;// ... 15 more files, same change, every one of them&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;One dependency added to a base class. Twenty files changed. Zero new behaviour delivered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With a service based approach, this problem doesn't exist. The auditing logic lives in its own injected service. Only the classes that actually need auditing ask for it. Adding it to more places is opt in, not a cascading update across the whole hierarchy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why you should not mix the two
&lt;/h2&gt;

&lt;p&gt;On its own, having to check whether a piece of logic comes from a base class or an injected service is a small task. But you don't do it once. You do it for every class you touch, every time you touch it. That compounds. Over a large enough codebase, that constant context switching quietly eats a significant chunk of your day.&lt;/p&gt;

&lt;p&gt;The mental models behind the two approaches are also not equally complex. A dependency graph is straightforward: a class declares what it needs, the container provides it, you can see the full picture from the constructor. Inheritance is a different story. Behaviour is hidden up the chain. Logic you didn't write and didn't ask for shows up in your class because something three levels up decided to put it there. And if that base class itself extends another base class, which extends another — and yes, this happens — you're now archaeology, not programming.&lt;/p&gt;

&lt;p&gt;That layered inheritance problem deserves its own callout. Base classes having base classes having base classes is not a theoretical concern. It shows up in real projects, usually gradually, one "it made sense at the time" abstraction at a time. By the time someone new joins the team, understanding what a single service actually does requires tracing a chain that nobody fully holds in their head anymore.&lt;/p&gt;

&lt;p&gt;Keeping everything in the dependency graph doesn't eliminate complexity, but it keeps it visible. What a class does is what its constructor says it does. No hidden behaviour, no invisible ancestors, no archaeology required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one exception worth naming
&lt;/h2&gt;

&lt;p&gt;There's a legitimate use of inheritance that services and interfaces don't fully replace: extending framework types. ASP.NET controllers extending &lt;code&gt;ControllerBase&lt;/code&gt;. EF entities occasionally extending a common audit base. That kind of thing is mostly an API contract imposed by the framework, not a design choice you're making yourself.&lt;/p&gt;

&lt;p&gt;That's fine. The principle is about your domain logic — the business layer, the services, the orchestration. Don't build inheritance hierarchies there when your DI container is already set up to do the job more transparently.&lt;/p&gt;




&lt;p&gt;The promise of both inheritance and dependency injection is the same thing: write something once, use it many places, swap behaviour without rewriting callers. If you have a DI container, you already have the infrastructure to deliver on that promise. You don't need another mechanism fighting for the same ground.&lt;/p&gt;

&lt;p&gt;Keep the hierarchy flat. Let the container wire things together. Your future self will be able to read it faster.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your /Models folder is lying to you</title>
      <dc:creator>ThatGhost</dc:creator>
      <pubDate>Wed, 25 Mar 2026 12:32:25 +0000</pubDate>
      <link>https://dev.to/thatghost/your-models-folder-is-lying-to-you-25i0</link>
      <guid>https://dev.to/thatghost/your-models-folder-is-lying-to-you-25i0</guid>
      <description>&lt;p&gt;We've been trained to separate our POCOs into a neat folder. I'm here to tell you that folder is making your codebase harder to understand, not easier.&lt;/p&gt;

&lt;p&gt;Open any .NET solution made by a developer who learned "clean architecture" from a blog post and you'll find it. The &lt;code&gt;Models/&lt;/code&gt; folder. Or maybe it's called &lt;code&gt;DTOs/&lt;/code&gt;, or &lt;code&gt;Entities/&lt;/code&gt;, or &lt;code&gt;ViewModels/&lt;/code&gt;. Inside: 40, 50, sometimes 80 files. A graveyard of classes, half of which you have no idea whether anyone still uses.&lt;/p&gt;

&lt;p&gt;This pattern is so common it's treated as a given. And I think it's quietly making our code worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The promise vs. the reality
&lt;/h2&gt;

&lt;p&gt;The idea sounds reasonable: keep all your data classes in one place so they're easy to find. But what you actually get is a folder full of context-free classes with zero indication of where they belong or what they're for.&lt;/p&gt;

&lt;p&gt;When you open &lt;code&gt;UserSummaryDto.cs&lt;/code&gt;, do you know which service uses it? Which endpoint returns it? Whether it's still alive? You don't — you have to go hunting.&lt;/p&gt;

&lt;p&gt;The folder doesn't organize things. It just physically separates data from behavior and calls it architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every data object is the same
&lt;/h2&gt;

&lt;p&gt;The real problem with the &lt;code&gt;Models/&lt;/code&gt; folder is that it treats all POCOs as if they are the same kind of thing. They are not. Before deciding where a model lives, it helps to think about what role it actually plays.&lt;/p&gt;

&lt;p&gt;Some models are purely internal to a single service. They are implementation details, as private as any helper method. Nobody outside that service should ever know they exist.&lt;/p&gt;

&lt;p&gt;Some models flow between classes or layers within the same feature. They are part of an internal contract, not quite private but still scoped to a specific area of the codebase.&lt;/p&gt;

&lt;p&gt;Some models are genuinely shared across multiple services or features. They represent a concept that belongs to the whole application, and they deserve a clearly visible, shared location.&lt;/p&gt;

&lt;p&gt;And some models are not really independent objects at all. They are logical subsections of a bigger model, like a name inside a user or an address inside an order. They only exist because their parent exists.&lt;/p&gt;

&lt;p&gt;Throwing all four of these into the same &lt;code&gt;Models/&lt;/code&gt; folder collapses real structural differences into a flat list of files. The folder tells you nothing about scope, ownership, or lifetime. That information just disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat internal models like private functions
&lt;/h2&gt;

&lt;p&gt;Think about how you handle private functions. If a method is only used inside one class, you don't move it somewhere else "just in case." You keep it private, right where it belongs. Models should work the same way.&lt;/p&gt;

&lt;p&gt;If a POCO is only ever used inside one service, it should be a private nested class inside that service. Not a separate file. Not in a shared folder. Private. Scoped. Invisible to the rest of the codebase, just like a private method would be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;GetSummaryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&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="c1"&gt;// class and its properties only relevant to UserService, never to the outside&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserSummary&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;LastActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps file count down and makes the intent obvious. Anyone reading that code immediately knows: this model belongs here and nowhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the model outgrows the service
&lt;/h2&gt;

&lt;p&gt;Once a model needs to be accessible beyond the service itself, it earns its own file. At that point there are two approaches I use depending on how many models a service owns.&lt;/p&gt;

&lt;p&gt;If the service has one or two models, a single companion file is enough. It sits right next to the service and the relationship is obvious from the name alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Features
  /Users
    UserService.cs
    UserService.Models.cs   // owns all DTOs for this service
  /Orders
    OrderService.cs
    OrderService.Models.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the service grows and owns several distinct models, you can split them out into individual companion files, one per DTO, all still named after the service that owns them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Features
  /Users
    UserService.cs
    UserService.User.cs         // the core user DTO
    UserService.UserSettings.cs // settings owned by UserService
    UserService.UserAuth.cs     // auth data owned by UserService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, there is no &lt;code&gt;Models/&lt;/code&gt; folder at the root. Every file is named after the service that owns it, so you always know where a model comes from and who is responsible for it. The ownership is encoded in the file name itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested classes for nested data
&lt;/h2&gt;

&lt;p&gt;The same principle applies when one class is conceptually part of another. Take a &lt;code&gt;User&lt;/code&gt; that has a name made up of a first and last name. You could create a separate &lt;code&gt;UserName.cs&lt;/code&gt; or &lt;code&gt;NameUser.cs&lt;/code&gt; file, but that naming is already a hint that something is off. The class only exists because of &lt;code&gt;User&lt;/code&gt;. It has no independent identity.&lt;/p&gt;

&lt;p&gt;So just nest it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="n"&gt;FullName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Name&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Last&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the relationship is explicit in the code itself. You reference it as &lt;code&gt;User.Name&lt;/code&gt;, which reads naturally and makes the ownership clear. You haven't added a file, you haven't invented a weird compound class name, and the data is as close to its parent as it can be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about shared models?
&lt;/h2&gt;

&lt;p&gt;This is the fair counterargument. Some models genuinely are shared across services and they deserve a shared location. But that should be the exception, not the default. Most models in most codebases are not actually shared. They just live in &lt;code&gt;Models/&lt;/code&gt; because that's where models go.&lt;/p&gt;

&lt;p&gt;If a POCO is used by one service, it belongs to that service. Putting it in a central folder doesn't make it more reusable. It just makes it harder to find, harder to delete, and harder to understand at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I use
&lt;/h2&gt;

&lt;p&gt;When I create a new model I ask: is this used in more than one place today? If no, it becomes a private nested class. If it later needs to be shared, I'll promote it then. That's a much better signal than defaulting everything into a shared folder on day one.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
