<?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: Dynamite Technology</title>
    <description>The latest articles on DEV Community by Dynamite Technology (@dynamite_technology).</description>
    <link>https://dev.to/dynamite_technology</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%2F3933762%2F5110ce43-b67a-4c77-b0a2-e8c197a12b82.png</url>
      <title>DEV Community: Dynamite Technology</title>
      <link>https://dev.to/dynamite_technology</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dynamite_technology"/>
    <language>en</language>
    <item>
      <title>Scaling a Multi-Brand SaaS Platform Without Losing Your Mind: How We Built It</title>
      <dc:creator>Dynamite Technology</dc:creator>
      <pubDate>Fri, 15 May 2026 18:50:22 +0000</pubDate>
      <link>https://dev.to/dynamite_technology/scaling-a-multi-brand-saas-platform-without-losing-your-mind-how-we-built-it-o4n</link>
      <guid>https://dev.to/dynamite_technology/scaling-a-multi-brand-saas-platform-without-losing-your-mind-how-we-built-it-o4n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbpu4wv57euoogzgfpoq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbpu4wv57euoogzgfpoq.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;br&gt;
You're the architect of a SaaS platform. Your sales team just closed three major clients—each with their own branding requirements, custom workflows, and backend integrations. Your CTO asks: "Can we make this work without spinning up separate deployments?"&lt;/p&gt;

&lt;p&gt;Your answer used to be "maybe," and it would involve a lot of &lt;code&gt;if (brand === "...")&lt;/code&gt; statements scattered across your codebase.&lt;/p&gt;

&lt;p&gt;We've been there. And we found a better way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Nightmare: Conditional Architecture Hell
&lt;/h2&gt;

&lt;p&gt;When we first started supporting multiple brands, our approach was... let's call it "direct."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;brand&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dynamite&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DynamiteCard&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restaurant&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RestaurantCard&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pharmacy&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PharmacyCard&lt;/span&gt; &lt;span class="o"&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 problems started immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impossible to maintain&lt;/strong&gt;: Every new feature meant touching dozens of conditional branches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tight coupling&lt;/strong&gt;: A change for one client could break another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding nightmare&lt;/strong&gt;: Adding the fifth brand meant rewriting half the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dangerous to modify&lt;/strong&gt;: You couldn't refactor anything without fear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We called it &lt;strong&gt;Conditional Architecture Hell&lt;/strong&gt;, and every team that's scaled a multi-tenant SaaS has lived through it.&lt;/p&gt;

&lt;p&gt;The real question wasn't "can we support multiple brands?" It was "can we support them &lt;em&gt;elegantly&lt;/em&gt;?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Answer: A Hybrid Architecture
&lt;/h2&gt;

&lt;p&gt;Instead of building separate systems for each brand, we designed a single platform that could bend to different requirements without breaking.&lt;/p&gt;

&lt;p&gt;Our solution combines four key patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hexagonal Architecture&lt;/strong&gt; — Business logic stays isolated from external dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry Pattern&lt;/strong&gt; — Dynamic component and action resolution without conditionals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin Architecture&lt;/strong&gt; — Brand-specific code lives in isolated plugin modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter Pattern&lt;/strong&gt; — Different backends speak a common language (Unified DTOs)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gave us one codebase, one deployment, and infinite flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: The 30,000-Foot View
&lt;/h2&gt;

&lt;p&gt;Here's the basic flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   ↓
Next.js Frontend (Single App)
   ↓
Brand Plugin Layer (Dynamic Resolution)
   ↓
Backend API Gateway
   ↓
Brand Adapter Factory (Normalize Everything)
   ↓
Database / External APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer is designed to solve one specific problem. Let's break it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Frontend — The Registry Pattern
&lt;/h2&gt;

&lt;p&gt;The frontend is the first place where the magic happens. Instead of hardcoding which component belongs to which brand, we use a &lt;strong&gt;registry&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Brand Detection via Subdomains
&lt;/h3&gt;

&lt;p&gt;We identify tenants through subdomains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dynamite.domain.com
restaurant.domain.com
pharmacy.domain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our middleware extracts the brand name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes the tenant context for the entire request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Components Are Registered, Not Hardcoded
&lt;/h3&gt;

&lt;p&gt;Instead of conditionals, we register components by brand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;registerRecordCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dynamite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DynamiteCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;registerRecordCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restaurant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RestaurantCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;registerMenuAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Live Track&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LiveTrackAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;registerMenuAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Start Delivery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StartDeliveryAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the frontend needs to render something, it doesn't check &lt;code&gt;if brand === X&lt;/code&gt;. It just asks the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CardComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRecordCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&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;ActionComponents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMenuActions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why This Pattern Changed Everything
&lt;/h3&gt;

&lt;p&gt;The registry pattern eliminated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endless conditionals&lt;/td&gt;
&lt;td&gt;Dynamic resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard to onboard new clients&lt;/td&gt;
&lt;td&gt;Register and go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risky refactoring&lt;/td&gt;
&lt;td&gt;Isolated logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't scale beyond 10 brands&lt;/td&gt;
&lt;td&gt;Supports unlimited brands&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This single decision became one of the most important architectural choices we made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Backend — Hexagonal Architecture
&lt;/h2&gt;

&lt;p&gt;The frontend was half the battle. The backend was harder.&lt;/p&gt;

&lt;p&gt;Every brand had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different database schemas&lt;/li&gt;
&lt;li&gt;Different API endpoints&lt;/li&gt;
&lt;li&gt;Different query logic&lt;/li&gt;
&lt;li&gt;Different transformation rules&lt;/li&gt;
&lt;li&gt;Different business workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical query might look completely different for each tenant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DynamiteAdapter&lt;/span&gt;
&lt;span class="nx"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;FROM&lt;/span&gt; &lt;span class="nx"&gt;order_tracking&lt;/span&gt; &lt;span class="nx"&gt;WHERE&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// RestaurantAdapter&lt;/span&gt;
&lt;span class="nx"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;FROM&lt;/span&gt; &lt;span class="nx"&gt;delivery_orders&lt;/span&gt; &lt;span class="nx"&gt;WHERE&lt;/span&gt; &lt;span class="nx"&gt;completed_at&lt;/span&gt; &lt;span class="nx"&gt;IS&lt;/span&gt; &lt;span class="nx"&gt;NULL&lt;/span&gt;

&lt;span class="c1"&gt;// DataneralAdapter&lt;/span&gt;
&lt;span class="nx"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;FROM&lt;/span&gt; &lt;span class="nx"&gt;prescription_orders&lt;/span&gt; &lt;span class="nx"&gt;WHERE&lt;/span&gt; &lt;span class="nx"&gt;fulfillment_status&lt;/span&gt; &lt;span class="nc"&gt;IN &lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We couldn't just "configure" our way out of this. We needed a way to let each brand implement its own data fetching logic, but have everything speak the same language downstream.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Hexagonal Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Principle: Business Logic Shouldn't Know About Persistence
&lt;/h3&gt;

&lt;p&gt;In hexagonal architecture, your core business logic sits in the center and communicates with the outside world through &lt;strong&gt;ports&lt;/strong&gt; (interfaces). The actual database, APIs, and external services are &lt;strong&gt;adapters&lt;/strong&gt; that implement those ports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Domain (Pure Business Logic)
   ↓
Application Services
   ↓
Ports (Interfaces)
   ↓
Adapters (Database, APIs, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your business logic doesn't care if data comes from PostgreSQL, MongoDB, or an external ERP&lt;/li&gt;
&lt;li&gt;You can swap implementations without touching core logic&lt;/li&gt;
&lt;li&gt;Each brand can fetch data however it needs, as long as it conforms to the port interface&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Brand Adapters in Practice
&lt;/h3&gt;

&lt;p&gt;Each tenant gets its own adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DynamiteRecordAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getRecords&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`
      SELECT * FROM dynamite_orders 
      WHERE status NOT IN ('completed', 'cancelled')
    `&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;transformToUnified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;card_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BOOKING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_total_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PharmacyRecordAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getRecords&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`
      SELECT * FROM prescriptions 
      WHERE fulfillment_status = 'pending'
    `&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;transformToUnified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;card_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;medications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_total_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_charge&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;Each adapter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetches data its own way ✓&lt;/li&gt;
&lt;li&gt;Transforms data its own way ✓&lt;/li&gt;
&lt;li&gt;Returns a unified contract ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Normalization Layer: The Unsung Hero
&lt;/h3&gt;

&lt;p&gt;This became the most important backend layer. Everything flowing from the adapters gets normalized into &lt;strong&gt;Unified DTOs&lt;/strong&gt;:&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;UnifiedRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;card_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BOOKING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BILLING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRESCRIPTION&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;order_total_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;No matter where the data came from or what it looked like originally, the frontend sees the same structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Different Schemas
        ↓
Normalization Layer
        ↓
Unified DTO
        ↓
Frontend (No surprises)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single layer eliminated dozens of rendering bugs and made the frontend dramatically simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Configuration vs. Code — The Hybrid Model
&lt;/h2&gt;

&lt;p&gt;One of our biggest mistakes early on was trying to make &lt;em&gt;everything&lt;/em&gt; configurable.&lt;/p&gt;

&lt;p&gt;Some clients needed completely custom booking flows. Others needed live tracking integrations. Some wanted QR-code-based workflows. Making all of that configuration-driven would create an unmaintainable abstraction layer.&lt;/p&gt;

&lt;p&gt;Instead, we split the problem:&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Make Configurable
&lt;/h3&gt;

&lt;p&gt;Simple, declarative stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Themes and branding&lt;/li&gt;
&lt;li&gt;Menu structure and visibility&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;li&gt;Permissions and roles&lt;/li&gt;
&lt;li&gt;Invoice settings&lt;/li&gt;
&lt;li&gt;Module visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are declarative and safe to change without touching code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Stays Plugin-Based
&lt;/h3&gt;

&lt;p&gt;Complex, behavioral stuff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom workflows&lt;/li&gt;
&lt;li&gt;Advanced UI interactions&lt;/li&gt;
&lt;li&gt;Complex business logic&lt;/li&gt;
&lt;li&gt;Brand-specific experiences&lt;/li&gt;
&lt;li&gt;Custom integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these, we let engineers write code inside isolated plugin modules.&lt;/p&gt;

&lt;p&gt;This hybrid approach gave us the best of both worlds: flexibility for simple customizations and the power of code for complex ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Architecture
&lt;/h2&gt;

&lt;p&gt;By the end, we had built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared SaaS Core (Common functionality)
   +
Registry Pattern (Dynamic UI resolution)
   +
Plugin Adapters (Brand-specific code in isolation)
   +
Hexagonal Backend (Business logic stays pure)
   +
Unified DTO Contracts (All backends speak the same language)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Unlocked
&lt;/h2&gt;

&lt;p&gt;With this architecture in place, we were able to:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Onboard new brands faster&lt;/strong&gt; — No code duplication, no re-architecting&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Update the shared core safely&lt;/strong&gt; — Changes don't ripple across brands&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Support 10x more features&lt;/strong&gt; — Plugin system let brands innovate independently&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Reduce bugs dramatically&lt;/strong&gt; — Isolated plugin code, shared testing, unified contracts&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Scale the team&lt;/strong&gt; — Junior engineers could work on brand plugins without touching core&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Move faster&lt;/strong&gt; — No more "this change might break brand X" conversations&lt;/p&gt;

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

&lt;p&gt;The real lesson wasn't about any single pattern. It was about &lt;strong&gt;thoughtful separation of concerns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The registry pattern solved UI composition. Hexagonal architecture solved data access. Plugin architecture solved deployment scale. DTOs solved the integration seams.&lt;/p&gt;

&lt;p&gt;No single pattern would have worked alone. But together, they created something greater than the sum of their parts.&lt;/p&gt;

&lt;p&gt;If you're building a multi-tenant SaaS and you're drowning in &lt;code&gt;if (brand ===&lt;/code&gt; statements, know this: there's a better way. It takes more upfront thinking, but it pays for itself a hundred times over as you scale.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you built a multi-brand platform? What patterns worked for you? Share your thoughts in the comments below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>react</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
