<?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: Akintunde Morakinyo</title>
    <description>The latest articles on DEV Community by Akintunde Morakinyo (@akintunde_morakinyo_db6b2).</description>
    <link>https://dev.to/akintunde_morakinyo_db6b2</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2813810%2F413e041a-5070-4a35-9eb2-b00520863373.png</url>
      <title>DEV Community: Akintunde Morakinyo</title>
      <link>https://dev.to/akintunde_morakinyo_db6b2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akintunde_morakinyo_db6b2"/>
    <language>en</language>
    <item>
      <title>One Design Fits All—Until It Doesn’t</title>
      <dc:creator>Akintunde Morakinyo</dc:creator>
      <pubDate>Sun, 06 Sep 2026 11:18:18 +0000</pubDate>
      <link>https://dev.to/akintunde_morakinyo_db6b2/one-design-fits-all-until-it-doesnt-3a7p</link>
      <guid>https://dev.to/akintunde_morakinyo_db6b2/one-design-fits-all-until-it-doesnt-3a7p</guid>
      <description>&lt;h2&gt;
  
  
  Building reusable software without trapping every feature inside the same abstraction
&lt;/h2&gt;

&lt;p&gt;Most abstractions begin with a reasonable observation: we are repeating ourselves.&lt;/p&gt;

&lt;p&gt;Imagine an application with separate services for products, customers, categories, and orders. Each service performs similar operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nc"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementing these methods separately creates duplication. If the API convention changes, every service may need to be updated.&lt;/p&gt;

&lt;p&gt;A shared contract gives these services a consistent structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ICRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchParam&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PageCRUDService&lt;/code&gt; implements the &lt;code&gt;ICRUDService&lt;/code&gt; contract by providing the common behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PageCRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ICRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/GetAll`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="na"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Save`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="na"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Search`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&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;Creating a service becomes straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;PageCRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Product&lt;/span&gt;&lt;span class="dl"&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 is a useful abstraction. It reduces duplication, establishes a consistent API contract, and gives us one place to change shared behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Value of a Shared Contract
&lt;/h2&gt;

&lt;p&gt;Suppose every &lt;code&gt;GetAll&lt;/code&gt; endpoint initially returns an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, the API introduces server-side paging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PagedResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;TotalRecords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;Skipped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared contract changes to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PagedResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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 implementation changes in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;skip&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="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PagedResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/GetAll/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Product, customer, category, and order services do not each need a separate rewrite. They receive the new behaviour through the shared abstraction.&lt;/p&gt;

&lt;p&gt;That is where a generic design provides real value: it centralises stable knowledge about how the application communicates with its API.&lt;/p&gt;

&lt;p&gt;But reusable designs rarely remain simple forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When One Service Becomes Different?
&lt;/h2&gt;

&lt;p&gt;Suppose the product service later requires operations other services do not need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;GetLowStockProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nc"&gt;UploadProductImages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nc"&gt;GetProductsByCategory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One option is to keep expanding the common interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ICRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PagedResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;GetLowStockProducts&lt;/span&gt;&lt;span class="p"&gt;?():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;UploadImages&lt;/span&gt;&lt;span class="p"&gt;?():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ApproveOrder&lt;/span&gt;&lt;span class="p"&gt;?():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may work technically, but the abstraction has started losing its meaning.&lt;/p&gt;

&lt;p&gt;Product operations become visible to customer services. Order operations become visible to category services. Most specialised methods become optional because they do not apply to most consumers.&lt;/p&gt;

&lt;p&gt;The shared interface is no longer describing common behaviour. It is becoming a container for every possible behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extend Through Composition
&lt;/h2&gt;

&lt;p&gt;A cleaner approach is to compose the standard service with specialised behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crud&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;PageCRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Product&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;crud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;GetLowStockProducts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Product/GetLowStockProducts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;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;&lt;code&gt;ProductService&lt;/code&gt; retains the standard operations defined by &lt;code&gt;ICRUDService&amp;lt;Product&amp;gt;&lt;/code&gt; while adding its own specialised method.&lt;/p&gt;

&lt;p&gt;Other services remain focused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CategoryService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;PageCRUDService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Category&lt;/span&gt;&lt;span class="dl"&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 leads to an important principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A shared abstraction should contain what its consumers genuinely have in common—not everything any consumer might eventually need.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Generic UI Components Have the Same Problem
&lt;/h2&gt;

&lt;p&gt;Consider management pages that share a familiar workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display records in a table&lt;/li&gt;
&lt;li&gt;Search and filter records&lt;/li&gt;
&lt;li&gt;Open a Create or Edit dialog&lt;/li&gt;
&lt;li&gt;Save changes&lt;/li&gt;
&lt;li&gt;Delete records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generic management component can remove considerable duplication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ManagementPage&lt;/span&gt;
  &lt;span class="na"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Categories"&lt;/span&gt;
  &lt;span class="na"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;categoryFields&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;CategoryService&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works well for straightforward pages such as categories, roles, permissions, and warehouses.&lt;/p&gt;

&lt;p&gt;Then a more complex page arrives.&lt;/p&gt;

&lt;p&gt;A product page may require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple categories&lt;/li&gt;
&lt;li&gt;Multiple images&lt;/li&gt;
&lt;li&gt;A primary image&lt;/li&gt;
&lt;li&gt;Product variants&lt;/li&gt;
&lt;li&gt;Warehouse-specific prices&lt;/li&gt;
&lt;li&gt;Stock history&lt;/li&gt;
&lt;li&gt;Conditional validation&lt;/li&gt;
&lt;li&gt;A multi-step creation process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generic component can be expanded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ManagementPage&lt;/span&gt;
  &lt;span class="na"&gt;EnableImages&lt;/span&gt;
  &lt;span class="na"&gt;EnableVariants&lt;/span&gt;
  &lt;span class="na"&gt;ValidateBeforeSave&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;validateProduct&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;RenderExtraSection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderStockDetails&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some configuration is healthy. The danger begins when every requirement introduces another flag, callback, or special condition.&lt;/p&gt;

&lt;p&gt;Eventually, the generic component may contain code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Product-specific behaviour&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entityName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Order-specific behaviour&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the abstraction knows too much about its consumers. It becomes a central location where unrelated business rules are mixed together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should the Shared Abstraction Change?
&lt;/h2&gt;

&lt;p&gt;That depends on whether the new requirement is common or specific.&lt;/p&gt;

&lt;p&gt;If every service now returns paged results, update &lt;code&gt;ICRUDService&lt;/code&gt; and &lt;code&gt;PageCRUDService&lt;/code&gt;. That is a genuine change to the shared contract.&lt;/p&gt;

&lt;p&gt;If only products require image management, that behaviour should not automatically become part of every service or management page.&lt;/p&gt;

&lt;p&gt;A useful test is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this particular feature did not exist, would the requirement still belong in the shared abstraction?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, the behaviour probably belongs in specialised code or behind an extension point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Ways to Handle a Growing Difference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Configure the abstraction
&lt;/h3&gt;

&lt;p&gt;Configuration is appropriate when the difference is small and the standard workflow remains intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ManagementPage&lt;/span&gt;
  &lt;span class="na"&gt;BeforeSave&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;validateModel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;AfterSave&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;refreshSummary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Compose smaller building blocks
&lt;/h3&gt;

&lt;p&gt;When a page becomes substantially different, it can stop using the complete generic workflow while retaining smaller reusable components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageHeader&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchToolbar&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RightSideDialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;RightSideDialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page owns its business logic, while the table, search toolbar, and dialog remain reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a dedicated implementation
&lt;/h3&gt;

&lt;p&gt;If the workflow is fundamentally different, create a dedicated page or service.&lt;/p&gt;

&lt;p&gt;This is not a failure of the original abstraction. It means the use cases have diverged.&lt;/p&gt;

&lt;p&gt;The mistake would be forcing every feature through the same design merely to preserve architectural uniformity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Default, Extension, and Escape Principle
&lt;/h2&gt;

&lt;p&gt;A sustainable abstraction should provide three things.&lt;/p&gt;

&lt;h3&gt;
  
  
  A useful default
&lt;/h3&gt;

&lt;p&gt;The common case should require very little code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlled extension points
&lt;/h3&gt;

&lt;p&gt;Consumers should be able to customise selected behaviour without rewriting the shared implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  An affordable escape route
&lt;/h3&gt;

&lt;p&gt;A complex feature should be able to leave the abstraction without forcing the rest of the application to be rewritten.&lt;/p&gt;

&lt;p&gt;This last point is often overlooked.&lt;/p&gt;

&lt;p&gt;A design is not truly flexible simply because it accepts many options. It is flexible when a consumer can stop using it without causing architectural damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Little Duplication Can Be Cheaper
&lt;/h2&gt;

&lt;p&gt;Developers are often encouraged to remove duplication immediately. However, duplicated code and duplicated business concepts are not always the same thing.&lt;/p&gt;

&lt;p&gt;Two pages may look similar today while representing workflows that will evolve independently tomorrow.&lt;/p&gt;

&lt;p&gt;Combining them too early can create a false abstraction—one that initially saves a few lines but later requires conditions, flags, and exceptions to survive.&lt;/p&gt;

&lt;p&gt;Sometimes two small and clear implementations are cheaper than one highly configurable generic implementation.&lt;/p&gt;

&lt;p&gt;The goal is not to eliminate every repeated line. It is to avoid repeating stable knowledge while keeping changing business behaviour easy to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Reusable architecture is valuable, but uniformity should not become a goal of its own.&lt;/p&gt;

&lt;p&gt;A good abstraction standardises what is genuinely common.&lt;/p&gt;

&lt;p&gt;A better abstraction provides controlled ways to specialise its behaviour.&lt;/p&gt;

&lt;p&gt;A great abstraction also makes it inexpensive to walk away when the use cases are no longer the same.&lt;/p&gt;

&lt;p&gt;The goal is not to create one design that fits everything.&lt;/p&gt;

&lt;p&gt;The goal is to create useful defaults, honest extension points, and safe escape routes.&lt;/p&gt;

&lt;p&gt;A good abstraction saves code today.&lt;/p&gt;

&lt;p&gt;A great abstraction leaves room for tomorrow.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Suggested tags:&lt;/strong&gt; Software Architecture, TypeScript, React, Clean Code, Web Development&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>typescript</category>
      <category>react</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Multi-Tenancy Is More Than Adding a TenantId Column</title>
      <dc:creator>Akintunde Morakinyo</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:08:48 +0000</pubDate>
      <link>https://dev.to/akintunde_morakinyo_db6b2/multi-tenancy-is-more-than-adding-a-tenantid-column-1bdm</link>
      <guid>https://dev.to/akintunde_morakinyo_db6b2/multi-tenancy-is-more-than-adding-a-tenantid-column-1bdm</guid>
      <description>&lt;p&gt;Multi-tenancy often looks straightforward at the beginning.&lt;/p&gt;

&lt;p&gt;You have multiple organizations using the same application and database. Add a &lt;code&gt;TenantId&lt;/code&gt; to each record, automatically filter queries by the current tenant, and make sure every insert carries the correct tenant.&lt;/p&gt;

&lt;p&gt;Problem solved.&lt;/p&gt;

&lt;p&gt;At least, that was the assumption I started with while working on multi-tenancy support for &lt;strong&gt;SimpleORM.Net&lt;/strong&gt;, an ORM project I have been building for .NET.&lt;/p&gt;

&lt;p&gt;Then I encountered a simple question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should the Tenant model itself have a TenantId?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Obviously, it shouldn't.&lt;/p&gt;

&lt;p&gt;Then came another question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about Country?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If my application contains a list of countries, should every tenant have its own copy of Nigeria, Canada, Germany, the United Kingdom, and every other country?&lt;/p&gt;

&lt;p&gt;Again, probably not.&lt;/p&gt;

&lt;p&gt;That was when a seemingly simple feature became a much more interesting architectural problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Model
&lt;/h2&gt;

&lt;p&gt;A common approach to multi-tenancy is to make every business record tenant-aware.&lt;/p&gt;

&lt;p&gt;Conceptually, our models might look 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;Customer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DBModel&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;Name&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;TenantId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queries then automatically include the current tenant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;CurrentTenant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when a new customer is saved, the ORM automatically assigns the current tenant.&lt;/p&gt;

&lt;p&gt;This is useful because developers don't have to remember tenant filtering every time they write a query.&lt;/p&gt;

&lt;p&gt;Without centralized enforcement, it only takes one forgotten condition to create a serious data-isolation problem.&lt;/p&gt;

&lt;p&gt;So automatic tenant enforcement makes sense.&lt;/p&gt;

&lt;p&gt;But there is a problem with applying that rule universally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Every Model Belongs to a Tenant
&lt;/h2&gt;

&lt;p&gt;Consider these models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
Order
Invoice
Country
Currency
Tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Customer clearly belongs to a tenant.&lt;/p&gt;

&lt;p&gt;An Order probably belongs to a tenant.&lt;/p&gt;

&lt;p&gt;An Invoice probably belongs to a tenant.&lt;/p&gt;

&lt;p&gt;But Country is different.&lt;/p&gt;

&lt;p&gt;Nigeria doesn't become a different country because Tenant A and Tenant B are using the system.&lt;/p&gt;

&lt;p&gt;The same is true for many reference datasets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Country
Currency
Language
TimeZone
Industry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These records may legitimately be shared across every tenant.&lt;/p&gt;

&lt;p&gt;And then there is the most obvious exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making &lt;code&gt;Tenant&lt;/code&gt; require a &lt;code&gt;TenantId&lt;/code&gt; would mean that a tenant belongs to another tenant.&lt;/p&gt;

&lt;p&gt;That is clearly the wrong abstraction.&lt;/p&gt;

&lt;p&gt;The mistake is assuming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Multi-tenant application
        =
Every model is tenant-scoped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are two different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tenant Scope Is a Property of the Model
&lt;/h2&gt;

&lt;p&gt;A better mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Application
                      │
              Multi-Tenancy Enabled
                      │
          ┌───────────┴───────────┐
          │                       │
    Tenant-Scoped              Global
       Models                  Models
          │                       │
   Customer                   Country
   Order                      Currency
   Invoice                    Tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-tenancy can still be enabled for the application.&lt;/p&gt;

&lt;p&gt;But individual models determine whether tenant isolation applies to them.&lt;/p&gt;

&lt;p&gt;This changes the responsibility of the ORM.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is multi-tenancy enabled?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It needs to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is multi-tenancy enabled, and is this particular model tenant-scoped?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That small distinction makes the architecture considerably more flexible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Should This Information Live?
&lt;/h2&gt;

&lt;p&gt;Once I reached this point, another design decision appeared.&lt;/p&gt;

&lt;p&gt;How should a model declare that it is global?&lt;/p&gt;

&lt;p&gt;There are several possibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Marker Interface
&lt;/h3&gt;

&lt;p&gt;For example:&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;Country&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DBModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IGlobalModel&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 ORM could check whether a model implements &lt;code&gt;IGlobalModel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This works, but an interface normally communicates behaviour or a contract.&lt;/p&gt;

&lt;p&gt;In this case, we aren't really introducing behaviour.&lt;/p&gt;

&lt;p&gt;We're describing metadata about how the model should be persisted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Configuration
&lt;/h3&gt;

&lt;p&gt;Another approach is configuration:&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiTenancy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exclude&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exclude&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exclude&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tenant&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides excellent flexibility.&lt;/p&gt;

&lt;p&gt;It is particularly useful when an application needs to change ORM behaviour without modifying the model.&lt;/p&gt;

&lt;p&gt;However, it also means that understanding the behaviour of &lt;code&gt;Country&lt;/code&gt; requires finding the application's ORM configuration.&lt;/p&gt;

&lt;p&gt;Looking at the model alone doesn't tell you whether it is tenant-scoped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Model Metadata
&lt;/h3&gt;

&lt;p&gt;The third approach is to make tenant scope part of the model's metadata.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;GlobalModel&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;Country&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DBModel&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;or:&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TenantScoped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&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;Country&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DBModel&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 intent travels with the model.&lt;/p&gt;

&lt;p&gt;Wherever &lt;code&gt;Country&lt;/code&gt; is used, the ORM can inspect its metadata and know that tenant enforcement should not apply.&lt;/p&gt;

&lt;p&gt;For an ORM, I find this particularly attractive because tenant behaviour becomes part of the model definition in the same way that keys, uniqueness, table mappings and other persistence rules are model metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcement Still Belongs in the ORM
&lt;/h2&gt;

&lt;p&gt;Allowing global models should not mean pushing tenant filtering back to application developers.&lt;/p&gt;

&lt;p&gt;That would defeat one of the major advantages of ORM-level multi-tenancy.&lt;/p&gt;

&lt;p&gt;The application should still be able to write something conceptually as simple as:&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;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ORM determines that &lt;code&gt;Customer&lt;/code&gt; is tenant-scoped and effectively produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;CurrentTenant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But:&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;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same rule needs to apply consistently to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT
INSERT
UPDATE
DELETE
COUNT
SEARCH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That consistency matters.&lt;/p&gt;

&lt;p&gt;If SELECT applies tenant isolation but UPDATE doesn't, the architecture is still unsafe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saving Records Requires the Same Decision
&lt;/h2&gt;

&lt;p&gt;The distinction becomes equally important during writes.&lt;/p&gt;

&lt;p&gt;For a tenant-scoped model:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ABC Limited"&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;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The infrastructure can attach the active tenant automatically.&lt;/p&gt;

&lt;p&gt;But for:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Nigeria"&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;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;there should be no requirement for tenant information.&lt;/p&gt;

&lt;p&gt;This means tenant enforcement shouldn't simply be buried inside a generic &lt;code&gt;Save&amp;lt;T&amp;gt;()&lt;/code&gt; method as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MultiTenancyEnabled?
    → Require Tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should behave more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MultiTenancyEnabled?
        │
        ▼
Is T tenant-scoped?
    │          │
   Yes         No
    │          │
Apply       Continue
Tenant      Normally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference looks small in code.&lt;/p&gt;

&lt;p&gt;Architecturally, it is significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Data Introduces Another Question
&lt;/h2&gt;

&lt;p&gt;Once global models exist, another interesting scenario becomes possible.&lt;/p&gt;

&lt;p&gt;What if some records of a model are global while others are tenant-specific?&lt;/p&gt;

&lt;p&gt;Products are a good example.&lt;/p&gt;

&lt;p&gt;Imagine a platform provides standard products that every tenant can use, but tenants can also create their own products.&lt;/p&gt;

&lt;p&gt;Now the query might need to represent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TenantId = CurrentTenant
OR
TenantId IS NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a different problem from a completely global model such as Country.&lt;/p&gt;

&lt;p&gt;So there are potentially three concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tenant-Scoped Model
Every record belongs to one tenant.

Global Model
Records belong to no tenant and are available to everyone.

Shared Model
Global records may coexist with tenant-specific records.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would resist implementing all three simply because they are possible.&lt;/p&gt;

&lt;p&gt;An important part of architecture is knowing when &lt;strong&gt;not&lt;/strong&gt; to generalize.&lt;/p&gt;

&lt;p&gt;If the current requirement only needs tenant-scoped and global models, those are the concepts the system should implement.&lt;/p&gt;

&lt;p&gt;The design should leave room for shared records later without forcing that complexity into today's API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;The interesting part of this problem wasn't adding another attribute or another condition to a query.&lt;/p&gt;

&lt;p&gt;It was discovering that the original abstraction was slightly wrong.&lt;/p&gt;

&lt;p&gt;The first abstraction was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application is multi-tenant, therefore its data is tenant-scoped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better abstraction became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application supports multi-tenancy, while individual models define their tenancy behaviour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction prevents strange models such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tenant → TenantId
Country → TenantId
Currency → TenantId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while preserving automatic isolation where it actually matters.&lt;/p&gt;

&lt;p&gt;This is something I repeatedly encounter when designing reusable infrastructure.&lt;/p&gt;

&lt;p&gt;The first version of an abstraction often describes the most common case.&lt;/p&gt;

&lt;p&gt;The difficult part is discovering whether that common case is actually a universal rule.&lt;/p&gt;

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

&lt;p&gt;Adding &lt;code&gt;TenantId&lt;/code&gt; is easy.&lt;/p&gt;

&lt;p&gt;Designing where tenant boundaries belong is the harder problem.&lt;/p&gt;

&lt;p&gt;A good multi-tenancy implementation needs to answer at least three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which models belong to tenants?&lt;/li&gt;
&lt;li&gt;Which models are global?&lt;/li&gt;
&lt;li&gt;Where is tenant isolation enforced?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For SimpleORM.Net, the direction is to keep enforcement inside the ORM while allowing model metadata to determine whether a particular model participates in tenant isolation.&lt;/p&gt;

&lt;p&gt;That keeps application code simple without pretending that every piece of data has the same ownership model.&lt;/p&gt;

&lt;p&gt;And sometimes that is the difference between adding multi-tenancy as a feature and actually designing for multi-tenancy.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>backend</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Simplified My Backend Architecture for Business Applications</title>
      <dc:creator>Akintunde Morakinyo</dc:creator>
      <pubDate>Sat, 08 Aug 2026 21:36:20 +0000</pubDate>
      <link>https://dev.to/akintunde_morakinyo_db6b2/how-i-simplified-my-backend-architecture-for-business-applications-161l</link>
      <guid>https://dev.to/akintunde_morakinyo_db6b2/how-i-simplified-my-backend-architecture-for-business-applications-161l</guid>
      <description>&lt;p&gt;After more than a decade of building business applications, I noticed something interesting.&lt;/p&gt;

&lt;p&gt;Very few projects became difficult because the business rules were complicated.&lt;/p&gt;

&lt;p&gt;Most became difficult because of everything surrounding the business rules.&lt;/p&gt;

&lt;p&gt;Every new module seemed to require another controller, another service, another repository, another set of CRUD methods, another search endpoint, another authorization check and another audit implementation.&lt;/p&gt;

&lt;p&gt;Over time, the application became full of infrastructure code that looked remarkably similar across different modules.&lt;/p&gt;

&lt;p&gt;That made me ask a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can infrastructure be designed once so developers spend most of their time writing business logic instead of rewriting the same plumbing?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question completely changed how I approach backend architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Business
&lt;/h2&gt;

&lt;p&gt;When I build business applications, I try to separate business behaviour from infrastructure.&lt;/p&gt;

&lt;p&gt;Infrastructure includes things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CRUD&lt;/li&gt;
&lt;li&gt;Searching&lt;/li&gt;
&lt;li&gt;Pagination&lt;/li&gt;
&lt;li&gt;Auditing&lt;/li&gt;
&lt;li&gt;Multi-tenancy&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are necessary, but they are rarely unique to a specific module.&lt;/p&gt;

&lt;p&gt;Whether you're building Products, Customers, Orders or Suppliers, those capabilities are usually identical.&lt;/p&gt;

&lt;p&gt;Business logic should be where the application becomes different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the Architecture Small
&lt;/h2&gt;

&lt;p&gt;Instead of adding more layers, I started removing unnecessary ones.&lt;/p&gt;

&lt;p&gt;The architecture now looks something like this:&lt;/p&gt;

&lt;p&gt;HTTP Request&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
Controller&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
Business Service&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
BaseService&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
IDataService&lt;br&gt;
      │&lt;br&gt;
      ▼&lt;br&gt;
Database&lt;/p&gt;

&lt;p&gt;Each layer has one clear responsibility.&lt;/p&gt;

&lt;p&gt;Controllers receive requests.&lt;/p&gt;

&lt;p&gt;Business services implement business rules.&lt;/p&gt;

&lt;p&gt;The base service provides reusable application behaviour.&lt;/p&gt;

&lt;p&gt;The data service handles persistence.&lt;/p&gt;

&lt;p&gt;That simple separation has proven easier to maintain than having every service implement the same CRUD operations repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group by Business Capability
&lt;/h2&gt;

&lt;p&gt;Another change I made was grouping services around business capabilities rather than around every model.&lt;/p&gt;

&lt;p&gt;For example, instead of creating services simply because a model exists, I group related functionality into services such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product Service&lt;/li&gt;
&lt;li&gt;Order Service&lt;/li&gt;
&lt;li&gt;Inventory Service&lt;/li&gt;
&lt;li&gt;Customer Service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supporting models don't automatically require their own service unless they introduce their own business behaviour.&lt;/p&gt;

&lt;p&gt;That keeps the solution easier to navigate and reduces unnecessary abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching Shouldn't Require New Endpoints
&lt;/h2&gt;

&lt;p&gt;Search requirements tend to grow throughout the lifetime of an application.&lt;/p&gt;

&lt;p&gt;It often starts with a few simple endpoints and gradually expands into dozens of variations.&lt;/p&gt;

&lt;p&gt;Rather than creating a new endpoint every time another filter is needed, I prefer allowing the client to describe the search.&lt;/p&gt;

&lt;p&gt;The backend remains responsible for validating those criteria and translating them into efficient database queries.&lt;/p&gt;

&lt;p&gt;The API surface stays stable while search capabilities continue to grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Infrastructure Automatic
&lt;/h2&gt;

&lt;p&gt;Another principle I try to follow is making infrastructure happen automatically whenever possible.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant filtering&lt;/li&gt;
&lt;li&gt;Audit information&lt;/li&gt;
&lt;li&gt;Pagination&lt;/li&gt;
&lt;li&gt;Index creation&lt;/li&gt;
&lt;li&gt;Timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers shouldn't need to remember these concerns throughout the application.&lt;/p&gt;

&lt;p&gt;The framework should handle them consistently.&lt;/p&gt;

&lt;p&gt;That not only reduces repetitive code but also helps prevent subtle bugs caused by inconsistent implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not GraphQL or gRPC?
&lt;/h2&gt;

&lt;p&gt;GraphQL and gRPC are both excellent technologies.&lt;/p&gt;

&lt;p&gt;The choice depends on the problem you're solving.&lt;/p&gt;

&lt;p&gt;GraphQL shines when clients need fine-grained control over the shape of the data they retrieve.&lt;/p&gt;

&lt;p&gt;gRPC excels at high-performance communication between services.&lt;/p&gt;

&lt;p&gt;Many business applications, however, primarily need predictable APIs with flexible searching, strong authorization, auditing and maintainable business logic.&lt;/p&gt;

&lt;p&gt;In those cases, a well-designed REST architecture with reusable infrastructure often provides everything required while remaining straightforward to develop, test and maintain.&lt;/p&gt;

&lt;p&gt;The objective isn't to avoid newer technologies.&lt;/p&gt;

&lt;p&gt;It's to choose the simplest architecture that solves the problem well.&lt;/p&gt;

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

&lt;p&gt;One lesson I've learned is that complexity rarely arrives all at once.&lt;/p&gt;

&lt;p&gt;It accumulates.&lt;/p&gt;

&lt;p&gt;A duplicated CRUD method here.&lt;/p&gt;

&lt;p&gt;Another search endpoint there.&lt;/p&gt;

&lt;p&gt;A new authorization implementation in another module.&lt;/p&gt;

&lt;p&gt;Individually, they don't seem significant.&lt;/p&gt;

&lt;p&gt;Collectively, they become expensive to maintain.&lt;/p&gt;

&lt;p&gt;Good architecture isn't about adding more layers or adopting every new technology.&lt;/p&gt;

&lt;p&gt;It's about removing unnecessary repetition so developers can focus on solving business problems.&lt;/p&gt;

&lt;p&gt;That's the principle I now try to apply whenever I design a new business application.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What architectural decisions have helped you reduce repetitive code in your own backend applications? I'd be interested to hear the patterns that have worked well for you.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>dotnet</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building an Efficient Authentication Pipeline with ASP.NET Core, Playwright and Token Caching</title>
      <dc:creator>Akintunde Morakinyo</dc:creator>
      <pubDate>Sun, 02 Aug 2026 19:16:48 +0000</pubDate>
      <link>https://dev.to/akintunde_morakinyo_db6b2/building-an-efficient-authentication-pipeline-with-aspnet-core-playwright-and-token-caching-2fc1</link>
      <guid>https://dev.to/akintunde_morakinyo_db6b2/building-an-efficient-authentication-pipeline-with-aspnet-core-playwright-and-token-caching-2fc1</guid>
      <description>&lt;p&gt;Authentication is one of the most important parts of any application that integrates with external services. While many modern platforms expose OAuth APIs or SDKs for third-party developers, there are situations where an application needs to interact with an authentication flow that is primarily designed for browser users.&lt;/p&gt;

&lt;p&gt;Recently, while building a mobile application that integrates with an external football management platform, I needed to design an authentication architecture that was both reliable and efficient. Rather than focusing solely on obtaining an access token, the bigger engineering challenge was ensuring that users were not repeatedly authenticated on every request.&lt;/p&gt;

&lt;p&gt;This article explains the architecture I adopted and the design decisions behind it.&lt;/p&gt;




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

&lt;p&gt;The mobile application communicates with an ASP.NET Core backend, which in turn communicates with an external service requiring authenticated requests.&lt;/p&gt;

&lt;p&gt;A naïve implementation would attempt to authenticate every time a user opens the application. Besides introducing unnecessary latency, this approach would repeatedly perform browser automation and create avoidable load on both the application and the external service.&lt;/p&gt;

&lt;p&gt;The objective was therefore to design a solution that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticates users only when necessary;&lt;/li&gt;
&lt;li&gt;securely stores authentication information;&lt;/li&gt;
&lt;li&gt;reuses valid access tokens;&lt;/li&gt;
&lt;li&gt;refreshes authentication only after token expiry; and&lt;/li&gt;
&lt;li&gt;keeps the mobile client completely unaware of the authentication complexity.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────┐
│   React Native App    │
└──────────┬────────────┘
           │
           ▼
┌───────────────────────┐
│   ASP.NET Core API    │
└──────────┬────────────┘
           │
           ▼
┌───────────────────────┐
│      MongoDB Cache    │
└──────┬─────────┬──────┘
       │         │
 Token Exists?   │
       │         │
       ▼         ▼
 Return Token  Launch Playwright
                    │
                    ▼
          External Authentication
                    │
                    ▼
           Receive Access Token
                    │
                    ▼
          Store Token + Expiry
                    │
                    ▼
         Return Authenticated User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Authentication Flow
&lt;/h2&gt;

&lt;p&gt;The authentication pipeline follows a simple decision process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 – Check Existing User
&lt;/h3&gt;

&lt;p&gt;When a login request reaches the backend, the application first checks whether the user already exists in MongoDB.&lt;/p&gt;

&lt;p&gt;Instead of immediately attempting another login, the stored authentication information is inspected.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 – Validate Token Expiry
&lt;/h3&gt;

&lt;p&gt;Each stored authentication record contains an access token together with its expiration timestamp.&lt;/p&gt;

&lt;p&gt;If the token is still valid, the backend simply reuses it.&lt;/p&gt;

&lt;p&gt;This avoids unnecessary authentication while significantly reducing response time for returning users.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3 – Authenticate Only When Necessary
&lt;/h3&gt;

&lt;p&gt;If the user is logging in for the first time, or the existing token has expired, the backend performs a fresh authentication using Playwright.&lt;/p&gt;

&lt;p&gt;Once authentication succeeds, the new access token and its expiry time are stored for future requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Centralising Authentication
&lt;/h2&gt;

&lt;p&gt;One design decision that proved particularly useful was centralising authentication inside the backend.&lt;/p&gt;

&lt;p&gt;The React Native application simply submits user credentials once. After successful authentication, the backend becomes responsible for managing the lifecycle of the access token.&lt;/p&gt;

&lt;p&gt;This provides several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a simpler mobile application;&lt;/li&gt;
&lt;li&gt;reduced duplication of authentication logic;&lt;/li&gt;
&lt;li&gt;easier maintenance; and&lt;/li&gt;
&lt;li&gt;the flexibility to change authentication behaviour without updating the client application.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Automatic Token Injection
&lt;/h2&gt;

&lt;p&gt;Once a valid token is available, it is attached to the shared &lt;code&gt;HttpClient&lt;/code&gt; before any authenticated requests are made.&lt;/p&gt;

&lt;h2&gt;
  
  
  This means the remainder of the application can focus entirely on business logic without worrying about manually attaching authentication headers for every request.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why Cache Authentication?
&lt;/h2&gt;

&lt;p&gt;Caching authentication information offers several practical benefits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Performance
&lt;/h3&gt;

&lt;p&gt;Returning users avoid repeating the authentication process, resulting in noticeably faster login times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lower Infrastructure Cost
&lt;/h3&gt;

&lt;p&gt;Browser automation is significantly more resource-intensive than validating a stored token. Reusing valid tokens reduces server workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved User Experience
&lt;/h3&gt;

&lt;p&gt;Users experience a seamless login flow while the backend transparently manages authentication behind the scenes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaner Architecture
&lt;/h3&gt;

&lt;p&gt;Separating authentication management from business functionality keeps services easier to test and maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Designing authentication is about much more than obtaining an access token.&lt;/p&gt;

&lt;p&gt;A robust authentication system should answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When should a user authenticate?&lt;/li&gt;
&lt;li&gt;How long should authentication remain valid?&lt;/li&gt;
&lt;li&gt;When should cached credentials be reused?&lt;/li&gt;
&lt;li&gt;How can authentication complexity be hidden from client applications?&lt;/li&gt;
&lt;li&gt;How can token management remain maintainable as the application grows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By treating authentication as an architectural concern rather than simply a login feature, it becomes possible to build systems that are faster, cleaner and significantly easier to evolve.&lt;/p&gt;




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

&lt;p&gt;Every integration presents its own authentication challenges, but the underlying engineering principles remain consistent.&lt;/p&gt;

&lt;p&gt;By introducing token caching, validating token expiry before re-authentication, and centralising authentication management within the backend, I was able to create an architecture that is efficient, maintainable and scalable.&lt;/p&gt;

&lt;p&gt;The implementation discussed here was developed while building a mobile integration for a football management platform, but the same design principles can be applied to many applications that interact with external authenticated services.&lt;/p&gt;

&lt;p&gt;In future articles, I'll explore additional aspects of the project, including designing reusable API clients, integrating a React Native front end with the backend, and structuring applications for long-term maintainability.&lt;/p&gt;

</description>
      <category>dotnetcore</category>
      <category>aspnet</category>
      <category>playwright</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
