<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>JPanganiban</title>
    <link rel="stylesheet" href="/assets/built/screen.css?v=27a0789bcb">

    <meta name="description" content="Thoughts, published." />
    <link rel="icon" href="/favicon.png" type="image/png" />
    <link rel="canonical" href="https://jpanganiban.com/" />
    <meta name="referrer" content="no-referrer-when-downgrade" />
    <link rel="next" href="https://jpanganiban.com/page/2/" />
    
    <meta property="og:site_name" content="JPanganiban" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="JPanganiban" />
    <meta property="og:description" content="Thoughts, published." />
    <meta property="og:url" content="https://jpanganiban.com/" />
    <meta property="article:publisher" content="https://www.facebook.com/ghost" />
    <meta name="twitter:card" content="summary" />
    <meta name="twitter:title" content="JPanganiban" />
    <meta name="twitter:description" content="Thoughts, published." />
    <meta name="twitter:url" content="https://jpanganiban.com/" />
    <meta name="twitter:site" content="@ghost" />
    
    <script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "WebSite",
    "publisher": {
        "@type": "Organization",
        "name": "JPanganiban",
        "url": "https://jpanganiban.com/",
        "logo": {
            "@type": "ImageObject",
            "url": "https://jpanganiban.com/favicon.png",
            "width": 60,
            "height": 60
        }
    },
    "url": "https://jpanganiban.com/",
    "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "https://jpanganiban.com/"
    },
    "description": "Thoughts, published."
}
    </script>

    <meta name="generator" content="Ghost 4.35" />
    <link rel="alternate" type="application/rss+xml" title="JPanganiban" href="https://jpanganiban.com/rss/" />
    <script defer src="https://unpkg.com/@tryghost/portal@~1.14.0/umd/portal.min.js" data-ghost="https://jpanganiban.com/" crossorigin="anonymous"></script><style id="gh-members-styles">.gh-post-upgrade-cta-content,
.gh-post-upgrade-cta {
    display: flex;
    flex-direction: column;
    align-items: center;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    text-align: center;
    width: 100%;
    color: #ffffff;
    font-size: 16px;
}

.gh-post-upgrade-cta-content {
    border-radius: 8px;
    padding: 40px 4vw;
}

.gh-post-upgrade-cta h2 {
    color: #ffffff;
    font-size: 28px;
    letter-spacing: -0.2px;
    margin: 0;
    padding: 0;
}

.gh-post-upgrade-cta p {
    margin: 20px 0 0;
    padding: 0;
}

.gh-post-upgrade-cta small {
    font-size: 16px;
    letter-spacing: -0.2px;
}

.gh-post-upgrade-cta a {
    color: #ffffff;
    cursor: pointer;
    font-weight: 500;
    box-shadow: none;
    text-decoration: underline;
}

.gh-post-upgrade-cta a:hover {
    color: #ffffff;
    opacity: 0.8;
    box-shadow: none;
    text-decoration: underline;
}

.gh-post-upgrade-cta a.gh-btn {
    display: block;
    background: #ffffff;
    text-decoration: none;
    margin: 28px 0 0;
    padding: 8px 18px;
    border-radius: 4px;
    font-size: 16px;
    font-weight: 600;
}

.gh-post-upgrade-cta a.gh-btn:hover {
    opacity: 0.92;
}</style>
    <script defer src="/public/cards.min.js?v=27a0789bcb"></script>
    <link rel="stylesheet" type="text/css" href="/public/cards.min.css?v=27a0789bcb">
    <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-8X8PE66VZV"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-8X8PE66VZV');
</script><style>:root {--ghost-accent-color: #3e3e3e;}</style>
</head>

<body class="home-template is-head-left-logo has-serif-title">
<div class="gh-site">

    <header id="gh-head" class="gh-head gh-outer">
        <div class="gh-head-inner">
            <div class="gh-head-brand">
                <div class="gh-head-brand-wrapper">
                    <a class="gh-head-logo" href="https://jpanganiban.com">
                            JPanganiban
                    </a>
                </div>
                <button class="gh-search gh-icon-btn" aria-label="Search this site" data-ghost-search><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg></button>
                <button class="gh-burger"></button>
            </div>

            <nav class="gh-head-menu">
                <ul class="nav">
    <li class="nav-home nav-current"><a href="https://jpanganiban.com/">Home</a></li>
    <li class="nav-about"><a href="https://jpanganiban.com/about/">About</a></li>
</ul>

            </nav>

            <div class="gh-head-actions">
                        <button class="gh-search gh-icon-btn" aria-label="Search this site" data-ghost-search><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg></button>
            </div>
        </div>
    </header>

    
<main id="gh-main" class="gh-main">
        <article class="gh-article post">
            <header class="gh-article-header gh-canvas">
    <div class="gh-article-meta">
            <span class="gh-article-label">Latest issue</span>
        <time class="gh-article-date" datetime="2026-04-13">Apr 13, 2026</time>
    </div>

    <h1 class="gh-article-title">20 Tips for Designing and Evolving Database Systems</h1>

</header>

    <figure class="gh-article-image gh-canvas image-wide">
        <img
            srcset="/content/images/size/w300/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg 300w,
                    /content/images/size/w720/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg 720w,
                    /content/images/size/w960/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg 960w,
                    /content/images/size/w1200/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg 1200w,
                    /content/images/size/w2000/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg 2000w"
            sizes="(max-width: 1200px) 100vw, 1200px"
            src="/content/images/size/w1200/2026/04/amsterdam-city-archives-9tmKEDz03uw-unsplash.jpg"
            alt="20 Tips for Designing and Evolving Database Systems"
        >
            <figcaption>Photo by <a href="https://unsplash.com/@amsterdamcityarchives?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Amsterdam City Archives</a> on <a href="https://unsplash.com/photos/architectural-blueprint-of-a-multi-story-building-9tmKEDz03uw?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption>
    </figure>

<section class="gh-content gh-canvas">
    <p>I asked Claude to lay out the core principles for designing and evolving database systems. What came back was interesting enough to share. Some of it confirmed things I already believed. Some of it pushed back on them. Either way, it's a useful lens for thinking about one of the parts of software engineering that's hardest to undo once you get it wrong.</p><h2 id="schema-design">Schema Design</h2><p><strong>1. Model the domain, not the UI.</strong>Your schema reflects business reality, not whatever form or screen triggered the requirement. If a UI conflates two concepts, your schema shouldn't.</p><p><strong>2. Name things precisely.</strong><code>user_id</code> on a table that stores organizations is a lie. Names should be unambiguous across the whole system — <code>created_by_user_id</code>, <code>owner_org_id</code>. The schema is documentation.</p><p><strong>3. Normalize to understand, denormalize to perform.</strong>Start in 3NF. Deviate only when you have a measured reason, not a speculative one. Every denormalization is a debt you'll pay on every write.</p><p><strong>4. Every table needs a stable, surrogate primary key.</strong>Natural keys (emails, phone numbers, codes) change. Surrogate keys (UUIDs, sequences) don't. Use natural keys as unique constraints, not PKs.</p><p><strong>5. Timestamps are non-negotiable.</strong>Every table gets <code>created_at</code> and <code>updated_at</code> at minimum. You will always need them. The cost is nothing; the regret of not having them is enormous.</p><p><strong>6. Nullable columns are design questions, not defaults.</strong><code>NULL</code> means "unknown" or "not applicable." If you're using it for "not yet set," that's either a missing table or a missing state column. Be intentional.</p><hr><h2 id="integrity">Integrity</h2><p><strong>7. Let the database enforce what it knows.</strong>Foreign keys, unique constraints, not-null constraints, check constraints — push rules as close to the data as possible. Application-layer enforcement alone is not enough.</p><p><strong>8. Avoid soft deletes unless you have a real reason.</strong><code>is_deleted</code> flags scatter complexity everywhere — queries, indexes, joins. If you need an audit trail, use an audit/event table. If you need recoverability, use backups.</p><p><strong>9. Use enums and lookup tables deliberately.</strong>Database-level enums are hard to migrate. Lookup/reference tables are more flexible. Either way, make the valid value set explicit — never rely on implicit strings.</p><p><strong>10. One source of truth per fact.</strong>If the same fact can be derived or stored in two places, you will eventually have two different answers. Computed values belong in queries or materialized views, not as redundant columns.</p><hr><h2 id="evolution">Evolution</h2><p><strong>11. Migrations are code — version them, review them, test them.</strong>A migration in production is as consequential as a deploy. It should go through the same discipline: reviewed, tested on a copy of prod data, rollback-considered.</p><p><strong>12. Expand before you contract.</strong>When changing a schema: add the new thing first, migrate data, update the app, then remove the old thing. Never do a destructive change in a single step. This makes zero-downtime deployments possible.</p><p><strong>13. Never rename or drop without a deprecation window.</strong>A column rename is a breaking change if anything reads it directly. Mark it deprecated, add the new column, migrate, then drop — across separate deploys.</p><p><strong>14. Design for the query, not just the insert.</strong>Before you finalize a schema, write out the top 5–10 queries that will run against it. If they require tortured joins or full scans, the model is probably wrong.</p><p><strong>15. Indexes are a contract with your query patterns.</strong>An index you're not using is overhead on every write. Know what queries drive each index. When query patterns change, audit your indexes.</p><hr><h2 id="operations-scale">Operations &amp; Scale</h2><p><strong>16. Understand your cardinality before you index.</strong>An index on a boolean column is usually useless. Indexes shine on high-cardinality columns used in WHERE clauses and joins.</p><p><strong>17. Long-running transactions are a hazard.</strong>They hold locks, block migrations, and cause replication lag. Design operations to be short and bounded. Batch large mutations.</p><p><strong>18. Separate read and write concerns early.</strong>Not necessarily CQRS, but think about which tables are write-heavy vs. read-heavy. That shapes indexing strategy, replication topology, and caching decisions.</p><p><strong>19. Plan for the data you'll never delete.</strong>Audit logs, financial records, event streams — they grow forever. Design their storage and access patterns separately from operational tables from day one.</p><p><strong>20. The schema is a shared contract.</strong>Everyone who touches the database — app code, scripts, BI tools, migrations — is a consumer of that contract. Changes without coordination are breaking changes.</p><hr><p>The meta-principle behind all of these: <strong>a database schema is harder to change than application code, so it deserves more upfront thought and more conservative evolution.</strong> Speed in schema design is usually false economy.</p>
</section>


<footer class="gh-article-footer gh-canvas">
    <nav class="gh-navigation">
        <div class="gh-navigation-previous">
                <a class="gh-navigation-link u-hover reverse" href="/not-all-feedback-is-created-equal-the-importance-of-authentic-user-feedback-in-software-development/">
                    <span class="u-hover-arrow"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
    <path d="M20.547 22.107L14.44 16l6.107-6.12L18.667 8l-8 8 8 8 1.88-1.893z"></path>
</svg>
</span>
                    <span class="u-hover-arrow"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
    <path d="M20.547 22.107L14.44 16l6.107-6.12L18.667 8l-8 8 8 8 1.88-1.893z"></path>
</svg>
</span>
                    Previous<span class="gh-navigation-hide">&nbsp;issue</span>
                </a>
        </div>

        <a class="gh-navigation-middle" href="/page/2"><span>Browse</span> all issues</a>

        <div class="gh-navigation-next">
        </div>
    </nav>
</footer>        </article>
</main>


    <footer class="gh-foot gh-outer">
        <div class="gh-foot-inner gh-inner">
            <div class="gh-copyright">
                JPanganiban © 2026
            </div>
                <nav class="gh-foot-menu">
                    <ul class="nav">
    <li class="nav-data-privacy"><a href="https://jpanganiban.com/privacy/">Data &amp; privacy</a></li>
    <li class="nav-contact"><a href="https://jpanganiban.com/contact/">Contact</a></li>
</ul>

                </nav>
            <div class="gh-powered-by">
                <a href="https://ghost.org/" target="_blank" rel="noopener">Powered by Ghost</a>
            </div>
        </div>
    </footer>

</div>

    <div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="pswp__bg"></div>

    <div class="pswp__scroll-wrap">
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <div class="pswp__ui pswp__ui--hidden">
            <div class="pswp__top-bar">
                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
                <button class="pswp__button pswp__button--share" title="Share"></button>
                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>
        </div>
    </div>
</div>
<script src="/assets/built/main.min.js?v=27a0789bcb"></script>



</body>

</html>