<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Architecture · Jared Lynskey</title>
    <link>https://jared.lynskey.co.nz/en/tags/architecture/</link>
    <description>Short stories of value on software engineering and engineering leadership — performance reviews, hiring and scaling teams, CI/CD, DevOps monitoring and agentic AI. Written from Seoul.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>© 2026 Jared Lynskey</copyright>
    <lastBuildDate>Mon, 26 Oct 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/en/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>What Is a Decision Document? A Practical Guide with Templates</title>
      <link>https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/</guid>
      <pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>decision-making</category>
      <category>documentation</category>
      <category>ADR</category>
      <category>architecture</category>
      <category>team alignment</category>
      <description>A decision document records a choice at the moment it&#39;s made: the context, the options considered, and why one won, so nobody re-argues it six months later. How to write ADRs and RFCs that actually get used, with an example ADR and a template to steal.</description>
      <content:encoded><![CDATA[<p>Every team makes decisions. Most teams forget why they made them. Six months later someone asks &ldquo;why did we choose PostgreSQL over MongoDB?&rdquo;, nobody remembers the constraints that applied at the time, and the whole debate happens again, usually with worse attendance.</p>
<p>Decision documents fix that. Call them ADRs, RFCs, or decision logs — the label doesn&rsquo;t matter. What matters is writing down the context, the options, and the reasoning at the moment of the choice. Running a dev team this year has made me appreciate them more than ever: they&rsquo;re the difference between institutional memory and institutional folklore.</p>
<hr>

<h3 class="relative group">Why they&rsquo;re worth the effort
    <div id="why-theyre-worth-the-effort" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-theyre-worth-the-effort" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The obvious payoff is not having the same argument twice. &ldquo;Didn&rsquo;t we already decide on the API versioning strategy?&rdquo; With a clear record of what was decided and why, settled issues stay settled and the team spends its energy on execution instead of retracing its steps.</p>
<p>The quieter payoff shows up over time. Projects run for months or years, people join and leave, and what survives is the code — which tells you <em>what</em> was decided but never <em>why</em>. A decision doc preserves the reasoning: the constraints that existed, the alternatives that were rejected, who made the call. New joiners get context instead of folklore. Cross-team decisions get real buy-in, because everyone can see the factors being weighed rather than just the verdict. And naming a decision-maker and contributors means follow-through has an actual owner.</p>
<hr>

<h3 class="relative group">When to write one
    <div id="when-to-write-one" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#when-to-write-one" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Not every decision deserves a document. I reach for one when the decision is:</p>
<ul>
<li><strong>Expensive to reverse</strong> — technology stack choices (frameworks, databases, cloud providers), architectural patterns (microservices vs. monolith, event-driven vs. request-response), third-party vendors, security and compliance approaches</li>
<li><strong>Long-lived</strong> — API contracts that external systems depend on, data models that span services, breaking changes to established patterns</li>
<li><strong>Cross-team</strong> — anything needing buy-in from multiple teams or non-technical stakeholders, resource allocation and prioritisation calls</li>
<li><strong>The outcome of a <a href="https://jared.lynskey.co.nz/en/posts/2023/2023-10-18-spike/" >spike</a></strong> — when a time-boxed investigation wraps up, capture the findings and the resulting decision before they evaporate</li>
</ul>
<p>Skip the formality for easily reversible decisions, team-internal implementation details, and anything with minimal impact outside your immediate scope. A document nobody needed is just homework.</p>
<hr>

<h3 class="relative group">The lifecycle
    <div id="the-lifecycle" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-lifecycle" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
</span></span><span class="line"><span class="cl">│   Draft     │ ──► │   Review    │ ──► │  Approved   │ ──► │ Implemented │
</span></span><span class="line"><span class="cl">│  Proposed   │     │  Feedback   │     │  Accepted   │     │  Completed  │
</span></span><span class="line"><span class="cl">└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
</span></span><span class="line"><span class="cl">                           │
</span></span><span class="line"><span class="cl">                           ▼
</span></span><span class="line"><span class="cl">                    ┌─────────────┐
</span></span><span class="line"><span class="cl">                    │  Rejected/  │
</span></span><span class="line"><span class="cl">                    │  Superseded │
</span></span><span class="line"><span class="cl">                    └─────────────┘</span></span></code></pre></div></div>
<p>The author drafts the problem and the candidate solutions. Stakeholders review — this is the phase that earns the whole exercise, because it&rsquo;s where assumptions get challenged and blind spots surface. A decision-maker (usually a tech lead, architect, or product owner) makes the final call, the team implements, and the doc gets updated if reality forced changes along the way. Some documents die in review; others get superseded later as circumstances change. Mark both clearly, because a stale &ldquo;accepted&rdquo; doc is a trap for whoever finds it next.</p>
<hr>

<h3 class="relative group">What makes one worth reading
    <div id="what-makes-one-worth-reading" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-makes-one-worth-reading" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Start with the problem, not your favourite solution. What are we solving, why does it matter now, and what happens if we do nothing? If people don&rsquo;t agree on the problem, debating solutions is theatre.</p>
<p>Then present real options — at least two or three, even when you have a strong preference. For each one: a brief description, pros and cons, a rough effort estimate (T-shirt sizing is fine: S/M/L/XL), and the risks with how you&rsquo;d mitigate them. Documenting the alternatives is how future readers understand why the winner won.</p>
<p>Name the trade-offs out loud. &ldquo;We&rsquo;re choosing speed-to-market over architectural purity.&rdquo; &ldquo;We&rsquo;re accepting increased operational complexity for better scalability.&rdquo; &ldquo;We&rsquo;re prioritising developer experience over raw performance.&rdquo; Sentences like these do more for a future reader than pages of analysis.</p>
<p>Ground the discussion in something concrete: code snippets showing how a solution would look, diagrams of the system interactions, links to proof-of-concept work from <a href="https://jared.lynskey.co.nz/en/posts/2023/2023-10-18-spike/" >spikes</a>. Abstract discussions produce abstract decisions.</p>
<p>Finally, give it a deadline — when the decision must be made, who owes input by when, and what&rsquo;s blocked while it drifts. Decisions without deadlines drift forever.</p>
<hr>

<h3 class="relative group">Example: Architecture Decision Record (ADR)
    <div id="example-architecture-decision-record-adr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#example-architecture-decision-record-adr" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Here&rsquo;s a practical example of a decision document:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># ADR-001: API Authentication Strategy
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Status:**</span> Accepted
</span></span><span class="line"><span class="cl"><span class="gs">**Date:**</span> 2024-11-15
</span></span><span class="line"><span class="cl"><span class="gs">**Decision Maker:**</span> Sarah Chen (Platform Architect)
</span></span><span class="line"><span class="cl"><span class="gs">**Contributors:**</span> Backend Team, Security Team, Mobile Team
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Context
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Our public API currently uses API keys for authentication. As we expand to
</span></span><span class="line"><span class="cl">support third-party integrations and mobile apps, we need a more robust
</span></span><span class="line"><span class="cl">authentication mechanism that supports:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Token expiration and refresh
</span></span><span class="line"><span class="cl"><span class="k">-</span> Scoped permissions
</span></span><span class="line"><span class="cl"><span class="k">-</span> User-level authentication (not just service-level)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Decision
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We will implement OAuth 2.0 with JWT tokens for API authentication.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Options Considered
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 1: OAuth 2.0 with JWT (Recommended)
</span></span></span><span class="line"><span class="cl">**Description:** Industry-standard protocol with self-contained tokens
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Industry standard, well-documented | More complex initial implementation |
</span></span><span class="line"><span class="cl">| Self-contained tokens reduce database lookups | Tokens cannot be revoked instantly |
</span></span><span class="line"><span class="cl">| Broad library support | Requires refresh token management |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Medium (2-3 sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 2: Session-based Authentication
</span></span></span><span class="line"><span class="cl">**Description:** Traditional server-side sessions with cookies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Simple to implement | Not suitable for mobile apps |
</span></span><span class="line"><span class="cl">| Easy to revoke sessions | Requires sticky sessions or shared session store |
</span></span><span class="line"><span class="cl">| Familiar to most developers | Doesn&#39;t scale as well |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Small (1 sprint)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Option 3: Custom Token System
</span></span></span><span class="line"><span class="cl">**Description:** Build our own token-based authentication
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">| Pros | Cons |
</span></span><span class="line"><span class="cl">|------|------|
</span></span><span class="line"><span class="cl">| Fully customizable | Reinventing the wheel |
</span></span><span class="line"><span class="cl">| No external dependencies | Security risks from custom implementation |
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Effort:**</span> Large (4+ sprints)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Consequences
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mobile team can implement standard OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> We&#39;ll need to set up a token refresh mechanism
</span></span><span class="line"><span class="cl"><span class="k">-</span> API documentation will need updates for OAuth flows
</span></span><span class="line"><span class="cl"><span class="k">-</span> Existing API key users will need a migration path (6-month deprecation)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## References
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> [<span class="nt">OAuth 2.0 RFC 6749</span>](<span class="na">https://tools.ietf.org/html/rfc6749</span>)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal spike document: [<span class="nt">Authentication Options Spike</span>](<span class="na">/spikes/auth-spike-2024</span>)
</span></span><span class="line"><span class="cl">- Security team review: SEC-2024-042</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">A template to steal
    <div id="a-template-to-steal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#a-template-to-steal" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Use this as a starting point and trim it to fit your team:</p>
<table>
	<thead>
			<tr>
					<th><strong>Attribute</strong></th>
					<th><strong>Details</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Decision/Issue Name</strong></td>
					<td>[Clear, descriptive title]</td>
			</tr>
			<tr>
					<td><strong>Status</strong></td>
					<td>[Draft / In Review / Accepted / Rejected / Superseded]</td>
			</tr>
			<tr>
					<td><strong>Impact</strong></td>
					<td>[High / Medium / Low]</td>
			</tr>
			<tr>
					<td><strong>Owner</strong></td>
					<td>[Name of the person driving the decision]</td>
			</tr>
			<tr>
					<td><strong>Decision Maker</strong></td>
					<td>[Person with final authority]</td>
			</tr>
			<tr>
					<td><strong>Due Date</strong></td>
					<td>[Deadline for making the decision]</td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">Problem Statement
    <div id="problem-statement" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#problem-statement" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>What problem are we solving? Why does it matter? What&rsquo;s the cost of inaction?</em></p>

<h4 class="relative group">Background
    <div id="background" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#background" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>Provide context, history, and any relevant details. Link to related documents, previous decisions, or spike results.</em></p>

<h4 class="relative group">Constraints
    <div id="constraints" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#constraints" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>What limitations or requirements must any solution respect?</em></p>
<ul>
<li>Budget constraints</li>
<li>Timeline requirements</li>
<li>Technical constraints (existing systems, skill sets)</li>
<li>Compliance or security requirements</li>
</ul>

<h4 class="relative group">Solutions Considered
    <div id="solutions-considered" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#solutions-considered" aria-label="Anchor">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>Option</strong></th>
					<th><strong>Description</strong></th>
					<th><strong>Pros</strong></th>
					<th><strong>Cons</strong></th>
					<th><strong>Effort</strong></th>
					<th><strong>Risk</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>Option 1</strong></td>
					<td><em>Description</em></td>
					<td><em>Pro A, Pro B</em></td>
					<td><em>Con A, Con B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Low/Med/High</em></td>
			</tr>
			<tr>
					<td><strong>Option 2</strong></td>
					<td><em>Description</em></td>
					<td><em>Pro A, Pro B</em></td>
					<td><em>Con A, Con B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Low/Med/High</em></td>
			</tr>
			<tr>
					<td><strong>Option 3</strong></td>
					<td><em>Description</em></td>
					<td><em>Pro A, Pro B</em></td>
					<td><em>Con A, Con B</em></td>
					<td><em>S/M/L/XL</em></td>
					<td><em>Low/Med/High</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">Recommendation
    <div id="recommendation" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#recommendation" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>State your recommended option and summarize why it&rsquo;s the best choice given the constraints and trade-offs.</em></p>

<h4 class="relative group">Decision
    <div id="decision" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#decision" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>Document the final decision once made. If different from the recommendation, explain why.</em></p>

<h4 class="relative group">Consequences
    <div id="consequences" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consequences" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>What are the implications of this decision? What follow-up work is required?</em></p>

<h4 class="relative group">Action Items
    <div id="action-items" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#action-items" aria-label="Anchor">#</a>
    </span>
    
</h4>
<table>
	<thead>
			<tr>
					<th><strong>Action</strong></th>
					<th><strong>Owner</strong></th>
					<th><strong>Due Date</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><em>Action 1</em></td>
					<td><em>Name</em></td>
					<td><em>Date</em></td>
			</tr>
			<tr>
					<td><em>Action 2</em></td>
					<td><em>Name</em></td>
					<td><em>Date</em></td>
			</tr>
	</tbody>
</table>

<h4 class="relative group">References
    <div id="references" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#references" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><em>Links to related documents, external resources, spike results, or prior decisions.</em></p>
<hr>

<h3 class="relative group">Making the habit stick
    <div id="making-the-habit-stick" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#making-the-habit-stick" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Store the docs somewhere consistent — a dedicated folder in the repo, a wiki space — because a decision nobody can find might as well not exist. Keep them lightweight; a document that takes days to write won&rsquo;t get written. Reference them from code comments, commit messages and pull requests, so the &ldquo;why&rdquo; stays attached to the &ldquo;what&rdquo;. When a decision is superseded, don&rsquo;t delete the old doc — mark it superseded and link to the new one; the history is the point. And revisit old decisions in your <a href="https://jared.lynskey.co.nz/en/posts/2023/2023-09-01-retrospecs/" >retrospectives</a>: which ones held up, which you&rsquo;d make differently now.</p>
<p>The failure modes are just as predictable. Analysis paralysis: set a deadline, decide with the information you have, and remember most decisions can be revisited. Rubber-stamp reviews that never produce real critique add process without adding value. Orphaned documents that never lead to action are worse than no documents at all. Too much detail: the doc should cover the why and the what, not the how — implementation belongs in technical specs. And when stakeholders disagree, write the dissent down. The decision may stand anyway, but the objection is exactly the context someone will need a year from now.</p>
<p>Start with your next significant decision. Write it up, even if it&rsquo;s rough, and refine the process as you go. Every &ldquo;why did we choose X?&rdquo; meeting that doesn&rsquo;t happen because someone just read the ADR is an hour your team gets back.</p>
<hr>

<h3 class="relative group">Further Reading
    <div id="further-reading" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#further-reading" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>Related Articles on This Site:</strong></p>
<ul>
<li><a href="https://jared.lynskey.co.nz/en/posts/2023/2023-10-18-spike/" >Understanding Spikes in Agile Software Development</a> - gather information with a spike before making the big call</li>
<li><a href="https://jared.lynskey.co.nz/en/posts/2023/2023-09-01-retrospecs/" >Retrospectives: The Power of Feedback Loops</a> - review past decisions as part of continuous improvement</li>
<li><a href="https://jared.lynskey.co.nz/en/posts/2023/2023-10-08-planning-an-api/" >Planning an API</a> - decision-document thinking applied to API design</li>
<li><a href="https://jared.lynskey.co.nz/en/posts/2024/2024-01-04-githubforprojectmanagement/" >GitHub for Project Management</a> - keep decision documents next to your code</li>
</ul>
<p><strong>External Resources:</strong></p>
<ul>
<li><a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions"  target="_blank" rel="noreferrer">Documenting Architecture Decisions - Michael Nygard</a> - the original post that popularised ADRs</li>
<li><a href="https://adr.github.io/"  target="_blank" rel="noreferrer">ADR GitHub Organization</a> - tools, templates and examples for Architecture Decision Records</li>
<li><a href="https://www.industrialempathy.com/posts/design-docs-at-google/"  target="_blank" rel="noreferrer">Design Docs at Google</a> - how Google approaches design documentation</li>
<li><a href="https://rust-lang.github.io/rfcs/"  target="_blank" rel="noreferrer">RFC Process at Rust</a> - a well-documented RFC process from the Rust language</li>
<li><a href="https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/"  target="_blank" rel="noreferrer">Spotify&rsquo;s Decision Record Template</a> - Spotify&rsquo;s take on when to write an ADR</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-12-03-decision-docs/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Single-Tenant vs Multi-Tenant SaaS: When to Use Which</title>
      <link>https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/</guid>
      <pubDate>Sat, 06 Apr 2024 00:30:03 +0000</pubDate>
      <dc:creator>Me</dc:creator>
      <category>SaaS</category>
      <category>architecture</category>
      <category>single-tenant</category>
      <category>multi-tenant</category>
      <description>Single-tenant (multi-instance) vs multi-tenant SaaS: what each one means, how they compare on isolation, cost and operations, the options in between, when to use which, how to keep tenants apart in a shared database, and the tooling that makes an instance per customer survivable.</description>
      <content:encoded><![CDATA[<p>Single-tenant SaaS gives every customer their own copy of the application and its database. Multi-tenant SaaS runs every customer on one shared application and database, kept apart by a tenant ID that has to be checked on every query. Single-tenant buys isolation, per-customer control and easier compliance, and costs you a fleet of copies to run. Multi-tenant is cheaper and much simpler to operate, but keeping customers apart becomes your code&rsquo;s job. Most products start multi-tenant and offer a dedicated instance to the few customers who need one.</p>
<p>Every B2B SaaS runs into the tenancy question eventually, usually the day a big prospective customer asks whether their data can live somewhere separate from everyone else&rsquo;s. I&rsquo;ve been near this conversation at more than one company now, and the answer always comes down to two options: give each customer their own instance (single-tenant, multi-instance), or run everyone on one shared instance (multi-tenant, single-instance). Neither is &ldquo;correct&rdquo;. They just fail in different ways.</p>

<h2 class="relative group">Infrastructure Design
    <div id="infrastructure-design" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#infrastructure-design" aria-label="Anchor">#</a>
    </span>
    
</h2>
<figure class="diagram">
  <div role="img" aria-label="The difference between Single-Tenant and Multi-Tenant"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 790" aria-hidden="true" focusable="false">
<defs><marker id="tn-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="d-head" d="M0,0 L10,5 L0,10 z"/></marker></defs>
<text class="d-title" x="320" y="164" text-anchor="middle" font-size="42">Multi-instance</text>
<text class="d-sub" x="320" y="204" text-anchor="middle" font-size="24">single-tenant: a copy per customer</text>
<rect class="d-panel" x="12" y="234" width="616" height="304" rx="36"/>
<rect class="d-group" x="25" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="33" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="337.64" text-anchor="middle" font-size="24">Database</text>
<polygon class="d-badge" points="118.0,256.0 143.7,274.7 133.9,304.8 102.1,304.8 92.3,274.7"/>
<g class="d-icon">
<ellipse cx="118" cy="273" rx="10" ry="4"/>
<path d="M108,273 v15 a10,4 0 0 0 20,0 v-15 M108,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="33" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="118.0" y="475.28" text-anchor="middle" font-size="23">Application</text>
<polygon class="d-badge" points="118.0,394.0 143.7,412.7 133.9,442.8 102.1,442.8 92.3,412.7"/>
<g class="d-icon">
<rect x="110" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="114.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M114,411 v-4 M114,427 v4 M110,415 h-4 M126,415 h4 M118,411 v-4 M118,427 v4 M110,419 h-4 M126,419 h4 M122,411 v-4 M122,427 v4 M110,423 h-4 M126,423 h4"/>
</g>
<line class="d-line" x1="118" y1="508" x2="118" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="84" cy="710.88" r="14.44"/><path d="M58.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="152" cy="710.88" r="14.44"/><path d="M126.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="118" cy="704.0" r="19.0"/><path d="M84.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="227" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="235" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="337.64" text-anchor="middle" font-size="24">Database</text>
<polygon class="d-badge" points="320.0,256.0 345.7,274.7 335.9,304.8 304.1,304.8 294.3,274.7"/>
<g class="d-icon">
<ellipse cx="320" cy="273" rx="10" ry="4"/>
<path d="M310,273 v15 a10,4 0 0 0 20,0 v-15 M310,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="235" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="320.0" y="475.28" text-anchor="middle" font-size="23">Application</text>
<polygon class="d-badge" points="320.0,394.0 345.7,412.7 335.9,442.8 304.1,442.8 294.3,412.7"/>
<g class="d-icon">
<rect x="312" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="316.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M316,411 v-4 M316,427 v4 M312,415 h-4 M328,415 h4 M320,411 v-4 M320,427 v4 M312,419 h-4 M328,419 h4 M324,411 v-4 M324,427 v4 M312,423 h-4 M328,423 h4"/>
</g>
<line class="d-line" x1="320" y1="508" x2="320" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="286" cy="710.88" r="14.44"/><path d="M260.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="354" cy="710.88" r="14.44"/><path d="M328.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="320" cy="704.0" r="19.0"/><path d="M286.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<rect class="d-group" x="429" y="250" width="186" height="272" rx="22"/>
<rect class="d-box" x="437" y="280" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="337.64" text-anchor="middle" font-size="24">Database</text>
<polygon class="d-badge" points="522.0,256.0 547.7,274.7 537.9,304.8 506.1,304.8 496.3,274.7"/>
<g class="d-icon">
<ellipse cx="522" cy="273" rx="10" ry="4"/>
<path d="M512,273 v15 a10,4 0 0 0 20,0 v-15 M512,280 a10,4 0 0 0 20,0"/>
</g>
<rect class="d-box" x="437" y="418" width="170" height="86" rx="14"/>
<text class="d-label" x="522.0" y="475.28" text-anchor="middle" font-size="23">Application</text>
<polygon class="d-badge" points="522.0,394.0 547.7,412.7 537.9,442.8 506.1,442.8 496.3,412.7"/>
<g class="d-icon">
<rect x="514" y="411" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="518.5" y="415.5" width="7" height="7"/>
<path stroke-width="1.4" d="M518,411 v-4 M518,427 v4 M514,415 h-4 M530,415 h4 M522,411 v-4 M522,427 v4 M514,419 h-4 M530,419 h4 M526,411 v-4 M526,427 v4 M514,423 h-4 M530,423 h4"/>
</g>
<line class="d-line" x1="522" y1="508" x2="522" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="488" cy="710.88" r="14.44"/><path d="M462.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="556" cy="710.88" r="14.44"/><path d="M530.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="522" cy="704.0" r="19.0"/><path d="M488.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<text class="d-title" x="982" y="50" text-anchor="middle" font-size="42">Multi-tenant</text>
<text class="d-sub" x="982" y="88" text-anchor="middle" font-size="24">one shared instance for everyone</text>
<rect class="d-panel" x="698" y="104" width="568" height="434" rx="44"/>
<rect class="d-box" x="887" y="126" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="183.64" text-anchor="middle" font-size="24">Database</text>
<polygon class="d-badge" points="982.0,102.0 1007.7,120.7 997.9,150.8 966.1,150.8 956.3,120.7"/>
<g class="d-icon">
<ellipse cx="982" cy="119" rx="10" ry="4"/>
<path d="M972,119 v15 a10,4 0 0 0 20,0 v-15 M972,126 a10,4 0 0 0 20,0"/>
</g>
<line class="d-line" x1="982" y1="216" x2="982" y2="246" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<rect class="d-group" x="704" y="240" width="556" height="156" rx="30"/>
<rect class="d-box" x="714" y="282" width="536" height="90" rx="16"/>
<polygon class="d-badge" points="982.0,248.0 1007.7,266.7 997.9,296.8 966.1,296.8 956.3,266.7"/>
<g class="d-icon">
<rect x="974" y="265" width="16" height="16" rx="2"/>
<rect class="d-icon-fill" x="978.5" y="269.5" width="7" height="7"/>
<path stroke-width="1.4" d="M978,265 v-4 M978,281 v4 M974,269 h-4 M990,269 h4 M982,265 v-4 M982,281 v4 M974,273 h-4 M990,273 h4 M986,265 v-4 M986,281 v4 M974,277 h-4 M990,277 h4"/>
</g>
<rect class="d-box" x="722" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="806.0" y="347.92" text-anchor="middle" font-size="22">Application</text>
<rect class="d-box" x="898" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="982.0" y="347.92" text-anchor="middle" font-size="22">Application</text>
<rect class="d-box" x="1074" y="306" width="168" height="56" rx="12"/>
<text class="d-label" x="1158.0" y="347.92" text-anchor="middle" font-size="22">Application</text>
<rect class="d-box" x="887" y="440" width="190" height="86" rx="14"/>
<text class="d-label" x="982.0" y="497.28" text-anchor="middle" font-size="23">Load Balancer</text>
<polygon class="d-badge" points="982.0,404.0 1007.7,422.7 997.9,452.8 966.1,452.8 956.3,422.7"/>
<g class="d-icon">
<path d="M982,416 l6,9 h-12 z M982,425 v5 M972,433 h20 M972,433 v3 M982,433 v3 M992,433 v3"/>
<rect class="d-icon-fill" x="969" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="979" y="436" width="6" height="5"/>
<rect class="d-icon-fill" x="989" y="436" width="6" height="5"/>
</g>
<line class="d-line" x1="806" y1="364" x2="922" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="982" y1="364" x2="982" y2="398" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="1158" y1="364" x2="1042" y2="438" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<line class="d-line" x1="932" y1="528" x2="780" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="716" cy="710.88" r="14.44"/><path d="M690.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="784" cy="710.88" r="14.44"/><path d="M758.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="750" cy="704.0" r="19.0"/><path d="M716.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="982" y1="528" x2="982" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="948" cy="710.88" r="14.44"/><path d="M922.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1016" cy="710.88" r="14.44"/><path d="M990.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="982" cy="704.0" r="19.0"/><path d="M948.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
<line class="d-line" x1="1032" y1="528" x2="1184" y2="652" marker-start="url(#tn-ah)" marker-end="url(#tn-ah)"/>
<g class="d-person-back"><circle cx="1180" cy="710.88" r="14.44"/><path d="M1154.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person-back"><circle cx="1248" cy="710.88" r="14.44"/><path d="M1222.16,758 v-9.120000000000001 a25.84,21.28 0 0 1 51.68,0 v9.120000000000001 z"/></g>
<g class="d-person"><circle cx="1214" cy="704.0" r="19.0"/><path d="M1180.0,766 v-12.0 a34.0,28.0 0 0 1 68.0,0 v12.0 z"/></g>
</svg>
</div>
</figure>


<h3 class="relative group">Single tenant, multi-instance
    <div id="single-tenant-multi-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#single-tenant-multi-instance" aria-label="Anchor">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>Single Tenant, Multi-Instance</strong>: In this setup, multiple instances of the infrastructure serve multiple tenants.</span>
</div>

<p>The appeal here is isolation, and nearly every benefit flows from it. Each customer&rsquo;s data sits in its own environment, so security reviews and compliance conversations get a lot easier. A bad deploy or a heavy workload takes down one customer instead of all of them. You can scale, tune, and customise each instance independently, and dedicated resources mean nobody&rsquo;s performance depends on what a neighbouring tenant is doing. Maintenance can be scheduled per customer, which keeps downtime small and targeted, and if you price things sensibly you can match infrastructure cost to what each tenant actually uses.</p>
<p>The price you pay is that you&rsquo;re now running N copies of everything. Resource overhead adds up, configuration drifts between instances, and keeping versions and data in sync across the fleet becomes its own project. Scaling, isolation, and cost management still need real planning. The problems don&rsquo;t disappear; they multiply.</p>

<h3 class="relative group">Multi tenant, single instance
    <div id="multi-tenant-single-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#multi-tenant-single-instance" aria-label="Anchor">#</a>
    </span>
    
</h3>

  



<div
  
    class="flex px-4 py-3 rounded-md shadow bg-primary-100 dark:bg-primary-900"
  
  >
  <span
    
      class="text-primary-400 pe-3 flex items-center"
    
    >
    <span class="relative block icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M112.1 454.3c0 6.297 1.816 12.44 5.284 17.69l17.14 25.69c5.25 7.875 17.17 14.28 26.64 14.28h61.67c9.438 0 21.36-6.401 26.61-14.28l17.08-25.68c2.938-4.438 5.348-12.37 5.348-17.7L272 415.1h-160L112.1 454.3zM191.4 .0132C89.44 .3257 16 82.97 16 175.1c0 44.38 16.44 84.84 43.56 115.8c16.53 18.84 42.34 58.23 52.22 91.45c.0313 .25 .0938 .5166 .125 .7823h160.2c.0313-.2656 .0938-.5166 .125-.7823c9.875-33.22 35.69-72.61 52.22-91.45C351.6 260.8 368 220.4 368 175.1C368 78.61 288.9-.2837 191.4 .0132zM192 96.01c-44.13 0-80 35.89-80 79.1C112 184.8 104.8 192 96 192S80 184.8 80 176c0-61.76 50.25-111.1 112-111.1c8.844 0 16 7.159 16 16S200.8 96.01 192 96.01z"/></svg>
</span>
  </span>

  <span
    
      class="dark:text-neutral-300"
    
    ><strong>Multi Tenant, Single Instance</strong>: A single software application serves multiple tenants.</span>
</div>

<p>This is the default for a reason: one deployment, one database to back up, one version in production. It&rsquo;s efficient with resources, simple to operate, and every customer gets updates and security patches at the same moment.</p>
<p>The hard parts are the flip side of sharing. One tenant&rsquo;s heavy usage can drag down everyone&rsquo;s performance, per-customer customisation is limited, and data isolation now lives in your application code rather than your infrastructure, so a single bug can turn into a cross-tenant data leak. Scalability bottlenecks hit all customers at once, and maintenance needs more care because there&rsquo;s no &ldquo;just this customer&rdquo; blast radius anymore.</p>
<p><a href="https://www.scaleway.com/en/blog/saas-multi-tenant-vs-multi-instance-architectures/"  target="_blank" rel="noreferrer">Learn more about SaaS Solutions - Multi-instance vs. Multi-tenant Architectures</a></p>

<h2 class="relative group">Multi-tenant vs multi-instance
    <div id="multi-tenant-vs-multi-instance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#multi-tenant-vs-multi-instance" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>You&rsquo;ll often see this described as multi-tenant vs multi-instance instead, and it&rsquo;s the same choice seen from the other side. &ldquo;Tenant&rdquo; is about customers: how many share one copy of the software. &ldquo;Instance&rdquo; is about deployments: how many copies you run. A single-tenant product is a multi-instance deployment, one copy per customer, and a multi-tenant product is usually a single instance that everyone shares. So when someone asks for multi-instance, they&rsquo;re asking for single-tenancy.</p>
<p>In practice it&rsquo;s a range rather than two boxes, and most of the real decisions are about the database. At the most shared end, every customer&rsquo;s rows sit in the same tables with a <code>tenant_id</code> column, and every query filters on it. That&rsquo;s the cheapest to run and the easiest to get wrong. One step along, each customer gets their own schema inside a shared database, which gives you cleaner separation and per-customer backups, at the cost of running every migration once per schema. Further along, the application stays shared but each customer gets their own database: a popular middle ground, with strong data isolation and no need to run a whole stack per customer. At the far end is full single-tenancy, with the application and the database both dedicated. AWS&rsquo;s SaaS guidance calls the two ends silo and pool, and mixes of the two bridge, which is handy vocabulary when you&rsquo;re explaining your setup to a customer&rsquo;s security team.</p>

<h2 class="relative group">Single-tenant vs multi-tenant at a glance
    <div id="single-tenant-vs-multi-tenant-at-a-glance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#single-tenant-vs-multi-tenant-at-a-glance" aria-label="Anchor">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Single-tenant (multi-instance)</th>
					<th>Multi-tenant (single instance)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Data isolation</td>
					<td>Separate infrastructure per customer</td>
					<td>Enforced by your code, or by database policies</td>
			</tr>
			<tr>
					<td>Cost per customer</td>
					<td>High: each customer carries a whole stack</td>
					<td>Low: everyone shares one</td>
			</tr>
			<tr>
					<td>Operations</td>
					<td>A deployment, an upgrade and a dashboard per customer</td>
					<td>One of each</td>
			</tr>
			<tr>
					<td>Noisy neighbours</td>
					<td>None</td>
					<td>One heavy tenant can slow everyone down</td>
			</tr>
			<tr>
					<td>Customisation</td>
					<td>Per customer, even different versions</td>
					<td>Mostly configuration and feature flags</td>
			</tr>
			<tr>
					<td>Releases</td>
					<td>Rolled out customer by customer</td>
					<td>Everyone at once</td>
			</tr>
			<tr>
					<td>A bad deploy hits</td>
					<td>One customer</td>
					<td>Every customer</td>
			</tr>
			<tr>
					<td>Security reviews and data residency</td>
					<td>Easy to answer</td>
					<td>Takes more explaining</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">When to use which
    <div id="when-to-use-which" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#when-to-use-which" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Go multi-tenant unless something specific pushes you off it. It&rsquo;s the right default when you have lots of customers paying modest amounts, when people sign themselves up, and when your team is small enough that thirty environments would eat it alive.</p>
<p>Single-tenant earns its overhead in a few situations. Customers in health, finance and government often need their data physically separate, or kept in a particular country, and a dedicated instance is the simplest honest answer to that in a security review. A handful of large customers on enterprise contracts can carry the cost of their own stack, and often expect one. And when customers need their own release schedule, a pinned version or deep customisation, separate instances stop being overhead and become part of what you&rsquo;re selling.</p>
<p>Where most teams end up is both: a multi-tenant product for nearly everyone, and a dedicated instance as an enterprise tier for the few customers who need it and will pay for it. If the multi-tenant version carries a tenant ID everywhere from day one, moving one customer out to their own database later is a migration rather than a rewrite.</p>

<h2 class="relative group">Making multi-tenant isolation harder to break
    <div id="making-multi-tenant-isolation-harder-to-break" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#making-multi-tenant-isolation-harder-to-break" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The multi-tenant bug that keeps people up at night is the query that forgets its tenant filter. It doesn&rsquo;t fail. It quietly returns somebody else&rsquo;s data, and code review won&rsquo;t catch every one. If you&rsquo;re on Postgres, row-level security moves the check into the database, so a query that forgets the filter still only sees the current customer&rsquo;s rows:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">invoices</span><span class="w"> </span><span class="n">ENABLE</span><span class="w"> </span><span class="k">ROW</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">SECURITY</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">POLICY</span><span class="w"> </span><span class="n">tenant_isolation</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">invoices</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">USING</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;app.tenant_id&#39;</span><span class="p">)::</span><span class="n">uuid</span><span class="p">);</span></span></span></code></pre></div></div>
<p>Each request then sets its tenant inside a transaction with <code>SET LOCAL app.tenant_id = '…'</code>, so a pooled connection doesn&rsquo;t carry one customer&rsquo;s ID into the next request, and a request that never sets it gets an error rather than rows. Two catches: connect the app as a role that doesn&rsquo;t own the table, because owners skip these policies unless you also add <code>FORCE ROW LEVEL SECURITY</code>, and never as a superuser, which skips them regardless.</p>

<h2 class="relative group">Where to start for single-tenant?
    <div id="where-to-start-for-single-tenant" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#where-to-start-for-single-tenant" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>If you do go single-tenant, tooling decides whether it&rsquo;s manageable or a slow-motion disaster. You can&rsquo;t hand-manage thirty environments.</p>
<p>The core is containers plus infrastructure as code. Docker packages the app identically for every tenant, Kubernetes handles deploying and scaling all those instances, and Terraform, Pulumi, or AWS CloudFormation let you define a tenant&rsquo;s entire stack in code, so spinning up customer #31 is a pull request rather than a lost weekend. If you&rsquo;re living closer to the VM world, configuration management tools like Ansible, Chef, or Puppet fill the same role. A solid CI/CD pipeline (Jenkins, GitLab CI/CD) matters more than usual here, because every release has to roll out across every instance without babysitting.</p>
<p>Beyond that, it depends on your product&rsquo;s shape. Serverless (AWS Lambda, Azure Functions) can remove the server layer entirely for some workloads. Managed databases like Amazon RDS or Azure SQL Database save you from administering one database per tenant by hand. Monitoring with Prometheus, Grafana, or New Relic stops being optional once you have a fleet, because you want to know which instance is unhappy before the customer tells you. For identity, a service like Auth0 or Okta handles user management and access control without you rebuilding it per tenant. Whatever language you work in, there&rsquo;s probably a SaaS-oriented framework or library that saves you some of this plumbing, and any of the big clouds (AWS, Azure, Google Cloud Platform) will host all of it happily.</p>
<p>Don&rsquo;t buy the whole list on day one. Start with containers and IaC, add monitoring and IAM as tenants accumulate, and let actual pain drive the rest.</p>

<h2 class="relative group">The short version
    <div id="the-short-version" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-short-version" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Single-tenant, or multi-instance, means a separate copy of the application and database for each customer: strong isolation and per-customer control, and a fleet to run. Multi-tenant means one shared copy: cheap and simple, with isolation enforced in code. Most products should start multi-tenant, with a tenant ID on everything and the database enforcing it, and offer dedicated instances to the few customers who need them and will pay for them. If you do run single-tenant, containers and infrastructure as code are what keep it manageable.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2024/2024-04-07-singletenantsingleinstance/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
