<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Jens Roland</title>
    <link>https://jensroland.com</link>
    <description>Articles on software engineering, leadership, and architecture by Jens Roland</description>
    <language>en</language>
    <lastBuildDate>Mon, 13 Apr 2026 00:00:00 +0200</lastBuildDate>
    <atom:link href="https://jensroland.com/rss" rel="self" type="application/rss+xml" />
    <image>
      <url>https://jensroland.com/apple-touch-icon.png</url>
      <title>Jens Roland</title>
      <link>https://jensroland.com</link>
    </image>
    <item>
      <title>The Cascade Reclaimed</title>
      <link>https://jensroland.com/articles/214/the-cascade-reclaimed</link>
      <description>We spent fifteen years running from the cascade. Now CSS has quietly become a different language — one that makes a new paradigm possible.</description>
      <content:encoded><![CDATA[<p>We have spent fifteen years running from the cascade. First with naming conventions – BEM, SMACSS, OOCSS – bolting structure onto a language that didn’t enforce it. Then with build tools – CSS Modules, styled-components, Emotion – abandoning stylesheets entirely in favour of runtime JavaScript. And finally with Tailwind, which solved the collision problem by nuking the abstraction layer altogether, scattering <code>flex items-center justify-between px-4 py-2 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200</code> across every <code>&lt;div&gt;</code> in the codebase and calling it progress.</p>
<p>Every one of these was a rational response to a real problem. The cascade <em>was</em> dangerous at scale. Naming <em>was</em> fragile. Global stylesheets <em>did</em> break silently when teams grew. But every solution also threw away something valuable – separation of concerns, central design authority, semantic markup, small footprints, or all four at once.</p>
<p>It is 2026. CSS has quietly become a different language. Not through a single headline feature, but through the accumulation of capabilities that, taken together, change what is structurally possible:</p>
<ul>
<li><strong><code>@layer</code></strong> gives you explicit control over cascade priority, making specificity wars a solved problem.</li>
<li><strong><code>@scope</code></strong> contains styles to a DOM subtree without any naming convention, build tool, or runtime.</li>
<li><strong><code>@property</code></strong> lets you register typed, inheritable custom properties with defaults – turning the cascade into a contextual type system.</li>
<li><strong><code>:has()</code></strong> lets CSS inspect the contents and relationships of elements, eliminating most presentational classes.</li>
<li><strong>Native nesting</strong> removes the last major reason anyone reached for a preprocessor.</li>
<li><strong>Container queries</strong> let components respond to their own size rather than the viewport.</li>
<li><strong><code>@starting-style</code></strong> and the <code>allow-discrete</code> transition behaviour let you animate elements appearing and disappearing – no JS, no libraries.</li>
<li><strong>Styleable form elements</strong> (<code>&lt;select&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;details&gt;</code>) are finally arriving, killing one of the oldest reasons to reach for a component library.</li>
</ul>
<p>None of these existed in practical cross-browser form five years ago. Most shipped in the last two or three. Collectively, they make a new paradigm possible – one that reclaims everything we gave up without reintroducing the problems that drove us away.</p>
<p>This document describes that paradigm. It is opinionated. It is practical. And it is not a framework – it is a set of architectural principles that produce clean markup, collision-proof styles, central design authority, and a CSS footprint measured in kilobytes.</p>
<hr />
<h2>The five principles</h2>
<ol>
<li>
<p><strong>The markup is the source of truth.</strong> HTML should be semantic and nearly class-free. If you are adding a class purely to give CSS something to grab onto, that is a smell. Modern CSS can select on structure, content, attributes, and relationships. Let it.</p>
</li>
<li>
<p><strong>The cascade is an asset, not a liability.</strong> Layers make cascade priority explicit. Scopes make it bounded. Typed properties make it a contextual inheritance system. Stop fighting the cascade and start programming it.</p>
</li>
<li>
<p><strong>Design decisions are centralised.</strong> Every colour, spacing value, radius, and typographic choice flows from a single set of design tokens. Components consume tokens; they never hardcode values. A change at the token level propagates everywhere, including through utilities.</p>
</li>
<li>
<p><strong>Collisions are structurally impossible.</strong> Not prevented by discipline. Not prevented by naming. Prevented by the platform: <code>@layer</code> ordering resolves before specificity, and <code>@scope</code> boundaries prevent selectors from leaking. This is enforcement, not convention.</p>
</li>
<li>
<p><strong>Utility classes are for composition gaps, not for components.</strong> A thin layer of token-derived utilities – spacing, colour, visibility – handles the seams between composed blocks. If you are stacking more than two utilities on one element, extract a semantic rule. The utility layer is an escape valve, not an architecture.</p>
</li>
</ol>
<hr />
<h2>Part I: The layer stack</h2>
<p>Everything begins with an explicit layer order declared once, at the top of your root stylesheet:</p>
<pre><code class="language-css">@layer reset, tokens, layout, components, utilities, overrides;
</code></pre>
<p>This single line eliminates the entire category of “my styles are being overridden and I don’t know why.” Layers resolve in declared order regardless of specificity. A selector in <code>components</code> will never beat a selector in <code>utilities</code>, and a selector in <code>utilities</code> will never beat a selector in <code>overrides</code> – even if the component selector is <code>#app .sidebar nav &gt; ul &gt; li &gt; a:first-child</code> and the override selector is <code>.mt-l</code>.</p>
<p>No <code>!important</code>. No specificity hacks. No re-ordering your <code>&lt;link&gt;</code> tags and praying.</p>
<p>Each layer has a clear purpose:</p>
<p><strong>reset</strong> – Normalise browser defaults. This is your Meyer reset, your <code>box-sizing: border-box</code>, your margin zeroing. Written once, touched never.</p>
<p><strong>tokens</strong> – Design primitives. Colours, spacing scale, type scale, radii, shadows, motion timing. These are registered custom properties living on <code>:root</code> (or contextual overrides on <code>[data-theme]</code>, <code>[data-density]</code>, etc.). This is the single source of design truth.</p>
<p><strong>layout</strong> – Page-level structure. Your grid shells, your sticky headers, your sidebar-plus-content frames. These concern <em>where things go</em>, not <em>what things look like</em>.</p>
<p><strong>components</strong> – The visual identity of discrete UI elements. Cards, buttons, form fields, nav bars, dialogs. Each scoped to its own DOM subtree.</p>
<p><strong>utilities</strong> – A deliberately thin set of single-purpose helpers for composition gaps. Margins between siblings, text alignment in a one-off context, visually-hidden content. Never more than 40–60 rules total.</p>
<p><strong>overrides</strong> – The “break glass in emergency” layer. Page-specific tweaks, third-party widget fixes, temporary hacks with a <code>/* TODO: remove after redesign */</code> comment. By living in the highest layer, these always win without needing <code>!important</code>, and they are easy to find and audit.</p>
<h3>What this solves</h3>
<ul>
<li><strong>Specificity wars:</strong> Gone. Layer order is the first (and usually only) resolution mechanism.</li>
<li><strong>Ordering fragility:</strong> Gone. It does not matter which <code>&lt;link&gt;</code> tag comes first as long as everything is assigned to its layer.</li>
<li><strong><code>!important</code> escalation:</strong> Gone. The override layer exists precisely for this purpose, with no need for specificity nuclear options.</li>
<li><strong>“Where does this rule go?”:</strong> Answered by the layer name. A new developer can look at the layer declaration and understand the architecture in ten seconds.</li>
</ul>
<hr />
<h2>Part II: Scoped components</h2>
<p>Within the <code>components</code> layer, every component is wrapped in a <code>@scope</code> block:</p>
<pre><code class="language-css">@layer components {
  @scope (.card) {
    :scope {
      background: var(--surface);
      color: var(--on-surface);
      border-radius: var(--radius-m);
      padding: var(--space-m);
      container-type: inline-size;
    }

    .title {
      font-size: var(--text-lg);
      font-weight: 600;
      line-height: 1.25;
    }

    .body {
      line-height: 1.6;
    }

    .actions {
      display: flex;
      gap: var(--space-s);
      justify-content: flex-end;
    }
  }

  @scope (.badge) {
    :scope {
      font-size: var(--text-xs);
      padding: var(--space-xs) var(--space-s);
      border-radius: var(--radius-full);
      background: var(--color-accent);
      color: var(--on-accent);
    }
  }
}
</code></pre>
<p>Notice: <code>.title</code> inside <code>.card</code> and <code>.title</code> inside any other component are completely separate selectors. <code>@scope</code> ensures that the rules inside <code>@scope (.card)</code> only match elements that are descendants of <code>.card</code>. There is no collision, no leaking, no need for <code>.card__title</code> or <code>data-card-title</code> or any other workaround.</p>
<p>You can use short, readable class names – <code>.title</code>, <code>.body</code>, <code>.actions</code>, <code>.icon</code> – across your entire codebase without fear. This is what BEM was trying to achieve, but enforced by the browser rather than by human discipline.</p>
<h3>Scoping boundaries with <code>to</code></h3>
<p><code>@scope</code> also supports a lower boundary, which limits how deep the scope reaches. This prevents a component from styling the internals of a nested child component:</p>
<pre><code class="language-css">@scope (.card) to (.card, .badge, .dialog) {
  /* These rules will NOT penetrate into nested .card,
     .badge, or .dialog subtrees */
}
</code></pre>
<p>This is the compositional boundary that CSS Modules and Shadow DOM tried to provide, but without the tooling overhead of the former or the harsh isolation of the latter. Scoped styles participate in the cascade normally – they inherit, they can be overridden by higher layers – they just can’t <em>reach</em> into components that don’t belong to them.</p>
<h3>The case for <code>@scope</code> over class-prefix conventions</h3>
<p>You might be tempted to achieve similar isolation with <code>.card-title</code> instead of <code>@scope (.card) { .title {} }</code>. The difference is structural:</p>
<ul>
<li>If someone writes <code>.card-title</code> inside a nested component, it will match. <code>@scope</code> will not.</li>
<li>If someone accidentally omits the prefix, it silently becomes a global rule. <code>@scope</code> selectors are local by default.</li>
<li>BEM requires every developer to remember and apply the convention. Scope is enforced by the parser.</li>
</ul>
<p>Conventions are not architecture. <code>@scope</code> is architecture.</p>
<hr />
<h2>Part III: Typed design tokens</h2>
<p>Design tokens in most systems are untyped custom properties: <code>--color-primary: #3b82f6</code>. This works, but it has limitations – you cannot transition them, there is no validation, and nothing stops someone from writing <code>padding: var(--color-primary)</code> and getting a silent failure.</p>
<p><code>@property</code> fixes this:</p>
<pre><code class="language-css">@property --surface {
  syntax: &quot;&lt;color&gt;&quot;;
  inherits: true;
  initial-value: #ffffff;
}

@property --on-surface {
  syntax: &quot;&lt;color&gt;&quot;;
  inherits: true;
  initial-value: #1a1a1a;
}

@property --space-unit {
  syntax: &quot;&lt;length&gt;&quot;;
  inherits: true;
  initial-value: 0.25rem;
}

@property --density {
  syntax: &quot;&lt;number&gt;&quot;;
  inherits: true;
  initial-value: 1;
}

@property --radius-m {
  syntax: &quot;&lt;length&gt;&quot;;
  inherits: false;
  initial-value: 0.375rem;
}
</code></pre>
<h3>Why this matters</h3>
<p><strong>Type safety.</strong> A <code>&lt;color&gt;</code> property will reject a length value. A <code>&lt;number&gt;</code> property will reject a string. The browser enforces your design system’s type contracts.</p>
<p><strong>Animatability.</strong> Unregistered custom properties cannot be transitioned – the browser does not know their type and so cannot interpolate. Registered properties with <code>syntax: &quot;&lt;color&gt;&quot;</code> or <code>syntax: &quot;&lt;length&gt;&quot;</code> can be smoothly transitioned and animated. Theme switches can fade. Density changes can ease. No JavaScript.</p>
<p><strong>Explicit inheritance control.</strong> Some tokens should inherit (colours, density, typography), and some should not (border-radius, shadow depth). <code>@property</code> lets you declare this explicitly, preventing surprising inheritance chains.</p>
<p><strong>Initial values as documentation.</strong> The <code>initial-value</code> serves as both a fallback and a declaration of the design system’s default. If a component references <code>var(--surface)</code> and nobody has set it, it gets white. Not <code>unset</code>. Not an empty string. A typed, intentional default.</p>
<h3>Contextual theming through inheritance</h3>
<p>Because registered properties participate in the cascade’s inheritance, theming becomes DOM-structural rather than imperative:</p>
<pre><code class="language-css">@layer tokens {
  [data-theme=&quot;dark&quot;] {
    --surface: #1a1a1a;
    --on-surface: #e5e5e5;
    --surface-raised: #2a2a2a;
  }

  [data-density=&quot;compact&quot;] {
    --density: 0.65;
  }

  [data-density=&quot;spacious&quot;] {
    --density: 1.4;
  }
}
</code></pre>
<p>Now wrap any subtree in <code>&lt;section data-theme=&quot;dark&quot;&gt;</code> and every component inside it – buttons, cards, inputs, badges – adapts automatically. No theme prop drilling. No context providers. No runtime JS. The cascade does what it has always done: it passes values down the tree. You are just giving it better values to pass.</p>
<p>A component written once works in light mode, dark mode, compact density, spacious density, and any combination thereof – without knowing any of these contexts exist. This is the separation of concerns that inline styles (and Tailwind) fundamentally cannot achieve.</p>
<h3>The token scale</h3>
<p>Define your spacing scale as derivations from a base unit:</p>
<pre><code class="language-css">@layer tokens {
  :root {
    --space-unit: 0.25rem;
    --space-xs: calc(var(--space-unit) * 1);   /* 0.25rem */
    --space-s:  calc(var(--space-unit) * 2);   /* 0.5rem  */
    --space-m:  calc(var(--space-unit) * 4);   /* 1rem    */
    --space-l:  calc(var(--space-unit) * 6);   /* 1.5rem  */
    --space-xl: calc(var(--space-unit) * 8);   /* 2rem    */
    --space-2xl: calc(var(--space-unit) * 12); /* 3rem    */
  }
}
</code></pre>
<p>Every spacing value in the system derives from <code>--space-unit</code>. Multiplied by <code>--density</code>, you can scale the entire spatial system:</p>
<pre><code class="language-css">button {
  padding: calc(var(--space-s) * var(--density))
           calc(var(--space-m) * var(--density));
}
</code></pre>
<p>One number – <code>--density</code> – controls how tight or loose every component feels. One number – <code>--space-unit</code> – controls the underlying spatial grid. Central authority. Zero duplication. Maximum leverage.</p>
<hr />
<h2>Part IV: Content-relational styling</h2>
<p>This is the most radical departure from how we have written CSS for the last twenty years. The premise: most classes exist to tell CSS something CSS can now figure out for itself.</p>
<p><code>:has()</code> is a relational pseudo-class. It lets you select an element based on what it <em>contains</em>. Combined with structural pseudo-classes, attribute selectors, and native nesting, you can write CSS that responds to the shape of the DOM – not to manually applied labels.</p>
<h3>The semantic article</h3>
<pre><code class="language-html">&lt;article&gt;
  &lt;img src=&quot;hero.jpg&quot; alt=&quot;A coastal sunset&quot;&gt;
  &lt;h2&gt;The Quiet Coast&lt;/h2&gt;
  &lt;p&gt;Long-form body text here.&lt;/p&gt;
  &lt;footer&gt;
    &lt;time datetime=&quot;2026-03-15&quot;&gt;March 15, 2026&lt;/time&gt;
    &lt;span&gt;8 min read&lt;/span&gt;
  &lt;/footer&gt;
&lt;/article&gt;
</code></pre>
<pre><code class="language-css">@layer components {
  @scope (article) {
    :scope {
      padding: var(--space-m);
      background: var(--surface);
    }

    /* Article with a hero image: grid layout */
    :scope:has(&gt; img:first-child) {
      display: grid;
      grid-template-rows: 200px 1fr auto;

      &gt; img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        border-radius: var(--radius-m) var(--radius-m) 0 0;
      }
    }

    /* Article without an image: accent border */
    :scope:not(:has(&gt; img)) {
      border-left: 3px solid var(--color-accent);
    }

    /* Article containing a blockquote: pull-quote style */
    :scope:has(blockquote) {
      font-style: italic;
      background: var(--surface-muted);
    }

    /* Article followed by another article: tighter margin */
    :scope:has(+ article) {
      margin-bottom: var(--space-s);
    }
  }
}
</code></pre>
<p>The HTML contains no presentational classes. No <code>.article--with-hero</code>. No <code>.article--text-only</code>. No <code>.article--quote-variant</code>. The CSS reads the DOM structure and adapts. Add an image, the layout changes. Remove it, the border appears. The markup stays clean, semantic, and concerned only with content.</p>
<h3>Relational form styling</h3>
<pre><code class="language-html">&lt;form&gt;
  &lt;label&gt;
    Email
    &lt;input type=&quot;email&quot; required&gt;
  &lt;/label&gt;
  &lt;label&gt;
    Message
    &lt;textarea required&gt;&lt;/textarea&gt;
  &lt;/label&gt;
  &lt;button type=&quot;submit&quot;&gt;Send&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<pre><code class="language-css">@layer components {
  @scope (form) {
    :scope:has(:invalid) button[type=&quot;submit&quot;] {
      opacity: 0.5;
      cursor: not-allowed;
    }

    label:has(:focus-visible) {
      color: var(--color-accent);
    }

    label:has(:invalid:not(:placeholder-shown)) {
      color: var(--color-danger);
    }

    label:has(:valid) {
      color: var(--color-success);
    }
  }
}
</code></pre>
<p>No JavaScript. No form libraries. No state management. The submit button dims when any required field is invalid. Labels change colour based on validation state. All driven by the DOM state that the browser already tracks.</p>
<h3>When to still use classes and attributes</h3>
<p>Content-relational styling handles <em>structural</em> variations. But some distinctions are truly semantic – the difference between a primary and a destructive button, for instance, is not structural. For those, use data attributes:</p>
<pre><code class="language-html">&lt;button data-variant=&quot;primary&quot;&gt;Save&lt;/button&gt;
&lt;button data-variant=&quot;danger&quot;&gt;Delete&lt;/button&gt;
</code></pre>
<pre><code class="language-css">@scope (button) {
  :scope {
    padding: var(--space-s) var(--space-m);
    border-radius: var(--radius-m);
    font-weight: 500;
  }

  :scope[data-variant=&quot;primary&quot;] {
    background: var(--color-primary);
    color: var(--on-primary);
  }

  :scope[data-variant=&quot;danger&quot;] {
    background: var(--color-danger);
    color: var(--on-danger);
  }
}
</code></pre>
<p>Why <code>data-variant</code> rather than <code>.btn-primary</code>? Because data attributes carry semantic meaning – they describe <em>what something is</em>, not <em>what it should look like</em>. And because they are scoped by the <code>@scope</code> block, they cannot collide with data attributes on unrelated components. This is a deliberate choice: classes for scoping boundaries, data attributes for semantic variations, and <code>:has()</code> for structural variations. Each selection mechanism has a clear, non-overlapping purpose.</p>
<hr />
<h2>Part V: The utility layer</h2>
<p>Utilities are not the architecture. They are the mortar between bricks.</p>
<p>The utility layer is thin – 40 to 60 rules at most – generated directly from your design tokens. It covers three domains: spacing, colour, and visibility.</p>
<pre><code class="language-css">@layer utilities {
  /* Spacing -- derived from token scale */
  .mt-xs  { margin-top: var(--space-xs); }
  .mt-s   { margin-top: var(--space-s); }
  .mt-m   { margin-top: var(--space-m); }
  .mt-l   { margin-top: var(--space-l); }
  .mt-xl  { margin-top: var(--space-xl); }
  .mb-xs  { margin-bottom: var(--space-xs); }
  .mb-s   { margin-bottom: var(--space-s); }
  .mb-m   { margin-bottom: var(--space-m); }
  .mb-l   { margin-bottom: var(--space-l); }
  .mb-xl  { margin-bottom: var(--space-xl); }
  .gap-s  { gap: var(--space-s); }
  .gap-m  { gap: var(--space-m); }
  .gap-l  { gap: var(--space-l); }

  /* Colour -- text overrides */
  .text-muted   { color: var(--color-muted); }
  .text-accent  { color: var(--color-accent); }
  .text-danger  { color: var(--color-danger); }
  .text-success { color: var(--color-success); }

  /* Typography */
  .text-center { text-align: center; }
  .text-sm     { font-size: var(--text-sm); }
  .text-lg     { font-size: var(--text-lg); }
  .font-bold   { font-weight: 700; }

  /* Display and visibility */
  .visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    width: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
  }

  .flex       { display: flex; }
  .flex-col   { flex-direction: column; }
  .items-center { align-items: center; }
}
</code></pre>
<p>That is the entire utility layer. Fifty-odd rules. Compare this with Tailwind’s thousands.</p>
<h3>The discipline</h3>
<p>Utilities are for <strong>composition gaps</strong> – the space between components that no single component should own:</p>
<pre><code class="language-html">&lt;section&gt;
  &lt;article&gt;...&lt;/article&gt;
  &lt;article class=&quot;mt-l&quot;&gt;...&lt;/article&gt;  &lt;!-- gap between siblings --&gt;
&lt;/section&gt;

&lt;footer class=&quot;mt-xl text-center text-muted&quot;&gt;
  &lt;p&gt;&amp;copy; 2026 Acme Corp&lt;/p&gt;
&lt;/footer&gt;
</code></pre>
<p>If you find yourself writing <code>class=&quot;flex items-center gap-s text-sm text-muted font-bold&quot;</code>, you have built a component out of utilities. Stop. Extract it into a scoped component rule.</p>
<p>The two-utility rule: if an element needs more than two utilities, it likely deserves a semantic style. This is not a hard limit – it is a code smell threshold.</p>
<p>Because the utilities reference design tokens, a central change to <code>--space-m</code> propagates through both component styles and utilities. Central authority is preserved even in the escape hatch.</p>
<hr />
<h2>Part VI: Container queries and intrinsic layout</h2>
<p>Media queries ask “how wide is the viewport?” Container queries ask “how wide is the component?” This distinction matters enormously in component-based design, where the same card might appear in a narrow sidebar and a wide main content area within the same viewport.</p>
<pre><code class="language-css">@layer components {
  @scope (.card) {
    :scope {
      container-type: inline-size;
      /* ... base styles ... */
    }

    @container (min-width: 400px) {
      :scope {
        display: grid;
        grid-template-columns: 200px 1fr;
      }
    }

    @container (min-width: 700px) {
      :scope {
        grid-template-columns: 300px 1fr auto;
      }

      .actions {
        flex-direction: column;
        align-self: start;
      }
    }
  }
}
</code></pre>
<p>The card adapts to <em>its own available space</em>, not the screen. This is intrinsic design – the component carries its own layout logic. No breakpoint props. No size-variant classes. No JavaScript resize observers.</p>
<p>Combined with <code>@scope</code> and <code>:has()</code>, you reach a level of component autonomy that previously required a JavaScript framework. The component knows its boundaries (scope), its content (<code>:has()</code>), and its available space (container queries). All in CSS. All without a build step.</p>
<hr />
<h2>Part VII: Motion and transitions</h2>
<p>CSS can now handle most of the transitions that used to require JavaScript animation libraries.</p>
<h3>Entry and exit animations with <code>@starting-style</code></h3>
<pre><code class="language-css">dialog[open] {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 200ms ease, transform 200ms ease,
              display 200ms allow-discrete;

  @starting-style {
    opacity: 0;
    transform: translateY(-1rem);
  }
}

dialog:not([open]) {
  opacity: 0;
  transform: translateY(-1rem);
  transition: opacity 150ms ease, transform 150ms ease,
              display 150ms allow-discrete;
}
</code></pre>
<p>Dialogs, popovers, dropdown menus, toast notifications – all can animate in and out with CSS alone, using the native HTML elements and attributes. No Framer Motion. No GSAP. No <code>useTransition</code> hook.</p>
<p>The <code>allow-discrete</code> value lets you transition <code>display: none</code> to <code>display: block</code>, which was impossible until recently. Elements can now truly appear and disappear with fluid motion, not just opacity tricks over invisible-but-still-present DOM nodes.</p>
<h3>Token-driven motion</h3>
<p>Centralise your timing in the token layer:</p>
<pre><code class="language-css">@layer tokens {
  :root {
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
    --duration-fast: 100ms;
    --duration-normal: 200ms;
    --duration-slow: 400ms;
  }

  @media (prefers-reduced-motion: reduce) {
    :root {
      --duration-fast: 0ms;
      --duration-normal: 0ms;
      --duration-slow: 0ms;
    }
  }
}
</code></pre>
<p>By zeroing all durations under <code>prefers-reduced-motion</code>, every transition in your entire system respects the user’s motion preference – automatically, centrally, without any per-component logic.</p>
<hr />
<h2>Putting it all together</h2>
<p>Here is a minimal but complete example showing all principles working in concert.</p>
<h3><code>tokens.css</code></h3>
<pre><code class="language-css">@layer reset, tokens, layout, components, utilities, overrides;

@layer tokens {
  @property --surface {
    syntax: &quot;&lt;color&gt;&quot;;
    inherits: true;
    initial-value: #ffffff;
  }

  @property --on-surface {
    syntax: &quot;&lt;color&gt;&quot;;
    inherits: true;
    initial-value: #1a1a1a;
  }

  @property --color-accent {
    syntax: &quot;&lt;color&gt;&quot;;
    inherits: true;
    initial-value: #3b82f6;
  }

  @property --density {
    syntax: &quot;&lt;number&gt;&quot;;
    inherits: true;
    initial-value: 1;
  }

  :root {
    --space-unit: 0.25rem;
    --space-xs: calc(var(--space-unit) * 1);
    --space-s:  calc(var(--space-unit) * 2);
    --space-m:  calc(var(--space-unit) * 4);
    --space-l:  calc(var(--space-unit) * 6);
    --space-xl: calc(var(--space-unit) * 8);
    --radius-m: 0.375rem;
    --radius-full: 9999px;
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.25rem;
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --duration-normal: 200ms;
  }

  [data-theme=&quot;dark&quot;] {
    --surface: #1a1a1a;
    --on-surface: #e5e5e5;
  }

  [data-density=&quot;compact&quot;] {
    --density: 0.65;
  }

  @media (prefers-reduced-motion: reduce) {
    :root {
      --duration-normal: 0ms;
    }
  }
}
</code></pre>
<h3><code>components/card.css</code></h3>
<pre><code class="language-css">@layer components {
  @scope (.card) to (.card) {
    :scope {
      background: var(--surface);
      color: var(--on-surface);
      border-radius: var(--radius-m);
      padding: calc(var(--space-m) * var(--density));
      container-type: inline-size;
      transition: box-shadow var(--duration-normal) var(--ease-out);
    }

    :scope:hover {
      box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
    }

    :scope:has(&gt; img:first-child) {
      padding: 0;
      &gt; img {
        width: 100%;
        aspect-ratio: 16 / 9;
        object-fit: cover;
        border-radius: var(--radius-m) var(--radius-m) 0 0;
      }
      &gt; :not(img) {
        padding-inline: calc(var(--space-m) * var(--density));
      }
      &gt; :last-child {
        padding-bottom: calc(var(--space-m) * var(--density));
      }
    }

    .title {
      font-size: var(--text-lg);
      font-weight: 600;
    }

    .meta {
      font-size: var(--text-sm);
      color: color-mix(in srgb, var(--on-surface) 60%, transparent);
    }

    @container (min-width: 500px) {
      :scope:has(&gt; img:first-child) {
        display: grid;
        grid-template-columns: 200px 1fr;
        &gt; img {
          height: 100%;
          border-radius: var(--radius-m) 0 0 var(--radius-m);
        }
      }
    }
  }
}
</code></pre>
<h3>The HTML</h3>
<pre><code class="language-html">&lt;main&gt;
  &lt;section data-theme=&quot;dark&quot;&gt;
    &lt;article class=&quot;card&quot;&gt;
      &lt;img src=&quot;sunset.jpg&quot; alt=&quot;Coastal sunset&quot;&gt;
      &lt;h2 class=&quot;title&quot;&gt;The Quiet Coast&lt;/h2&gt;
      &lt;p&gt;Body text...&lt;/p&gt;
      &lt;p class=&quot;meta&quot;&gt;March 15, 2026 &amp;middot; 8 min read&lt;/p&gt;
    &lt;/article&gt;
  &lt;/section&gt;

  &lt;aside data-density=&quot;compact&quot;&gt;
    &lt;article class=&quot;card&quot;&gt;
      &lt;h2 class=&quot;title&quot;&gt;No Image Here&lt;/h2&gt;
      &lt;p&gt;This card has no hero image, so it gets different styling.&lt;/p&gt;
    &lt;/article&gt;
  &lt;/aside&gt;
&lt;/main&gt;
</code></pre>
<p>Count the classes: <code>card</code>, <code>title</code>, <code>meta</code>. Three. The card adapts to dark mode, compact density, presence or absence of images, and its own container width – all from CSS. The markup is readable. The styles are centralised. Collisions are impossible.</p>
<hr />
<h2>What we stopped needing</h2>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Old solution</th>
<th>This paradigm</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cascade collisions</td>
<td>BEM, CSS Modules, Tailwind</td>
<td><code>@layer</code> + <code>@scope</code></td>
</tr>
<tr>
<td>Naming conventions</td>
<td><code>.block__element--modifier</code></td>
<td>Short names inside <code>@scope</code></td>
</tr>
<tr>
<td>Theming</td>
<td>JS context providers, class toggling</td>
<td><code>@property</code> inheritance</td>
</tr>
<tr>
<td>Component variants</td>
<td>Presentational classes</td>
<td><code>:has()</code> + <code>data-*</code> attributes</td>
</tr>
<tr>
<td>Responsive components</td>
<td>Viewport media queries + JS</td>
<td>Container queries</td>
</tr>
<tr>
<td>Preprocessor variables</td>
<td>SASS/LESS <code>$variables</code></td>
<td>Native custom properties</td>
</tr>
<tr>
<td>Preprocessor nesting</td>
<td>SASS/LESS nesting</td>
<td>Native nesting</td>
</tr>
<tr>
<td>Entry/exit animation</td>
<td>JS animation libraries</td>
<td><code>@starting-style</code> + <code>allow-discrete</code></td>
</tr>
<tr>
<td><code>!important</code> wars</td>
<td>Rage</td>
<td><code>@layer overrides</code></td>
</tr>
<tr>
<td>Build step</td>
<td>Required</td>
<td>Optional</td>
</tr>
</tbody>
</table>
<hr />
<h2>What this is not</h2>
<p>This is not a framework. There is no <code>npm install</code>. There is no config file. There is no build step (though you can add one if you want file splitting or minification – that is your choice, not a requirement).</p>
<p>This is an architectural pattern. It tells you how to organise your CSS and what tools to reach for in each situation. The implementation is vanilla CSS. The browser is the runtime.</p>
<p>It does not ask you to unlearn CSS. It asks you to use the CSS that actually exists today, instead of the CSS you learned to work around in 2015.</p>
<hr />
<h2>What it asks of you</h2>
<p><strong>Stop adding classes reflexively.</strong> Before writing <code>class=&quot;...&quot;</code>, ask: can <code>:has()</code>, a structural pseudo-class, or a data attribute handle this?</p>
<p><strong>Stop fearing the cascade.</strong> You control it now. Layers are your priority system. Scopes are your boundaries. Properties are your type system. The cascade is not your enemy – it is your most powerful tool.</p>
<p><strong>Stop reaching for JavaScript to style things.</strong> Theme providers, resize observers, animation libraries, conditional class logic – most of these exist because CSS could not do the job. It can now.</p>
<p><strong>Accept the small imperfections.</strong> There will be edge cases where you need a utility class, or a manual class, or an override. That is fine. This is not about purity. It is about having a default posture that produces clean, scalable, maintainable CSS – and reaching for escape hatches only when you genuinely need them.</p>
<p>The cascade was never the problem. Our inability to control it was. That problem is solved.</p>
]]></content:encoded>
      <pubDate>Mon, 13 Apr 2026 00:00:00 +0200</pubDate>
      <guid isPermaLink="true">https://jensroland.com/articles/214/the-cascade-reclaimed</guid>
      <enclosure url="https://jensroland.com/assets/img/gen/covers/cascade-reclaimed.avif" type="image/avif" length="0" />
      <category>css</category>
      <category>web development</category>
      <category>frontend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Codebase That Lasts Twice As Long Costs Half As Much</title>
      <link>https://jensroland.com/articles/213/the-codebase-that-lasts-twice-as-long-costs-half-as-much</link>
      <description>How to slow the atrophy of your code and create software solutions that last longer - much longer</description>
      <content:encoded><![CDATA[<p>No codebase was meant to be legacy code. And yet, here you are, plowing through 5000+ line source files so littered with undocumented side effects that touching any line could cause a failure on the other side of the repository. Even with a valiant attempt at refactoring, a codebase like this will probably deteriorate faster than you can patch it. This is life in a software debtor’s prison.</p>
<p>Time to start fresh and begin building a brand new system! One that is clean and modern. <em>Version 2.0</em> we’ll call it. We will learn from our mistakes and get it right with an all new team.</p>
<p><em>This time it’ll be different.</em></p>
<h2>The Greenfield-Bedlam Cycle</h2>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/greenfield-bedlam-cycle-642.avif 642w,
    /assets/img/gen/articles/greenfield-bedlam-cycle-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/greenfield-bedlam-cycle-389.avif"
    alt="A Greenfield-Bedlam Cycle Spotted In The Wild, Dall-E 3"
    loading="lazy">
  <figcaption><em>A Greenfield-Bedlam Cycle Spotted In The Wild</em>, Dall-E 3</figcaption>
</figure>
<p>Take a deep breath.</p>
<p>That tangled codebase you’re looking at started its life with similar aspirations. And then reality hit: a quick fix here, a compromise there, some over-eager abstraction in the heat of the moment, and before long, it’s death by a thousand corner cuts.</p>
<p><strong>With stakeholders to please and deadlines to meet, the technical debt piles up, until all that remains of the original architecture is a chalk outline in the rough shape of a strategy design pattern.</strong></p>
<p>Once a codebase has been through the grinder of business for a fiscal year or two, the debt can become so expensive that a certain class of organization (that rhymes with <em>schmenterprise</em>) would rather just send it adrift on an ice floe and start over from scratch.</p>
<p>I call it the <em>Greenfield-Bedlam Cycle of enterprise software development</em>; when a large enterprise keeps building the same software solution over and over, only to have it become a nightmare of incalculable risk within months of deployment. When this happens, the old solution is scrapped and the cycle starts over with a brand new <a tabindex="0" href="https://en.wikipedia.org/wiki/Greenfield_project">Greenfield</a> project.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/eternal-waterfall-clouds-642.avif 642w,
    /assets/img/gen/articles/eternal-waterfall-clouds-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/eternal-waterfall-clouds-389.avif"
    alt="Bow before the Waterfall of Eternity, Dall-E 3"
    loading="lazy">
  <figcaption><em>Bow before the Waterfall of Eternity</em>, Dall-E 3</figcaption>
</figure>
<p>Even in lean organizations with stronger technical vision, I have encountered the view that non-trivial codebases cannot be kept clean and maintainable over longer timeframes. At least not in fast-evolving fields like front end web development, where the same <del>framework</del> <del>library</del> <em>meta-framework</em> will happily reinvent itself five times in ten years, each time creating a trail of dead remains of legacy code scattered across the web.</p>
<p>You can’t blame someone for having a cynical stance on the longevity of things when the very house they live in was built on quicksand.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/engineer-and-clown-642.avif 642w,
    /assets/img/gen/articles/engineer-and-clown-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/engineer-and-clown-389.avif"
    alt="Reacto The Clown Hopes You Weren't Doing Anything Important, Dall-E 3"
    loading="lazy">
  <figcaption><em>Reacto The Clown Hopes You Weren't Doing Anything Important</em>, Dall-E 3</figcaption>
</figure>
<h2>Confronting The Messy Reality</h2>
<p>How do we break the Greenfield-Bedlam Cycle? Can we somehow stop the atrophy of our codebases and create software solutions that last longer - much longer - and that remains functional, maintainable, and extensible for years or decades, without sacrificing development speed or relying on <a tabindex="0" href="https://medium.com/ingeniouslysimple/the-origins-of-the-10x-developer-2e0177ecef60">mythical 10x developers</a>? And can we achieve this not just occasionally by happy accident, but methodically, repeatably?</p>
<p>I believe so, and the key to unlocking this power is acknowledging the imperfections of technology and developers, and the <em>messy reality</em> that your code will meet once it hits production. As a developer or software architect, you cannot opt out of reality, so you have to design around it.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/reality-is-a-messy-place-tech-blonde-642.avif 642w,
    /assets/img/gen/articles/reality-is-a-messy-place-tech-blonde-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/reality-is-a-messy-place-tech-blonde-389.avif"
    alt="Why does my coffee machine have node_modules?, Dall-E 3"
    loading="lazy">
  <figcaption><em>Why does my coffee machine have node_modules?</em>, Dall-E 3</figcaption>
</figure>
<h3>Messy Reality Number 1: Requirements change</h3>
<p>All project requirements are based on assumptions and limited information. In a matter of hours, assumptions can be invalidated or leadership priorities can change. Perhaps you simply encounter overwhelming success and need to scale beyond your wildest projections.</p>
<p>How would you change your design to face the reality that requirements could change at any point in almost any way?</p>
<h3>Messy Reality Number 2: External circumstances change</h3>
<p>Any new codebase is designed based on a version of reality that will not exist in 18 months. Technological innovation could make part of your solution obsolete, a competitor could launch a killer app that pulls the rug out from under your product, security vulnerabilities may be uncovered, or dependent libraries are abandoned by their maintainers, forcing you to replace them.</p>
<p>In short: Reality <em>drifts</em></p>
<p>How would you change your design to face the reality that almost any technology it uses could require replacement on short notice?</p>
<h3>Messy Reality Number 3: Mistakes are made</h3>
<p>Even a stack of mind-boggling complexity can be maintainable by a highly skilled developer with in-depth knowledge of the tech, the domain, the patterns and paradigms. That person could never have an ‘off’ day; and they would always be given all the time and resources they need. Does that sound like the world of enterprise? I have it on good authority that it’s not the world of <a tabindex="0" href="https://www.forbes.com/advisor/investing/faang-stocks-mamaa/">FAANG</a> either.</p>
<p>In all likelihood, the developer assigned to work on your stack on a given day is going to be on a deadline or unfamiliar with some part of the domain or tech. They might be lazy (in the <em>“move fast and break things”</em> sense of the word), or just having a bad day. A lot of the developers who are going to work on your codebase in 18 months haven’t even been hired yet, and some are still in school or interviewing for their first consulting job.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/smug-consultant-cartoony-left-642.avif 642w,
    /assets/img/gen/articles/smug-consultant-cartoony-left-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/smug-consultant-cartoony-left-389.avif"
    alt="Let me tell you all about No-Code AI on the Quantum Blockchain, Dall-E 3"
    loading="lazy">
  <figcaption><em>Let me tell you all about No-Code AI on the Quantum Blockchain</em>, Dall-E 3</figcaption>
</figure>
<p>The fact is, your code is going to see some hasty workarounds in its life, and not even the most meticulously designed software architecture survives first contact with a junior developer. That is, not unless it was built to adapt and endure – to resist bad changes and even self-heal over time.</p>
<p>How would you change your design to face the reality that the developers maintaining your solution will be making mistakes like they’re going out of fashion?</p>
<h2>Principles of long-lived software design</h2>
<p>How do we architect systems to survive and even thrive in a messy reality?</p>
<h3>Principle 1: Know the developers</h3>
<blockquote>
<p><em>“People are part of the system. The design should match the user’s experience, expectations, and mental models.”</em></p>
<p class="source">~ <a tabindex="0" href="https://books.google.com/books?id=I-NOcVMGWSUC&amp;pg=PA85">Principles of computer system design: an introduction</a></p>
</blockquote>
<p>To achieve longevity in our solutions, we must consider the skill and time constraints of the end users (developers/maintainers) and design a system with the largest-possible <a tabindex="0" href="https://blog.codinghorror.com/falling-into-the-pit-of-success/">Pit of Success</a> for the average developer to stumble towards.</p>
<p>This often means “killing your darlings” by meeting the developers where they are, not where you wish they were. There could be a significant skill gap between where the team is today and where they need to be to maintain the system you’re proposing. Few companies will invest in a comprehensive training program to not only re-train your current developers, but also bring all future hires up to speed – including after you leave or get promoted to senior management?</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/smug-consultant-cartoony-642.avif 642w,
    /assets/img/gen/articles/smug-consultant-cartoony-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/smug-consultant-cartoony-389.avif"
    alt="I mix $500 Scotch with Monster Energy. It's what plants crave, Dall-E 3"
    loading="lazy">
  <figcaption><em>I mix $500 Scotch with Monster Energy. It's what plants crave</em>, Dall-E 3</figcaption>
</figure>
<p>Even if it breaks your heart; that cool technology you like probably has to go.</p>
<p>Knowing the developers can also mean creating frameworks or abstractions that remove boilerplate and flatten the learning curve, or relying on patterns and even naming conventions that are already familiar to the devs.</p>
<p>In 2014, I had to build a frontend framework for a team of backend developers, and rather than try to convert them all to JavaScript enthusiasts, I created an HTMX-like markup syntax and async DOM manipulation component allowing them to build and maintain interactive frontend features without ever having to leave their familiar C#.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/reduce-friction-642.avif 642w,
    /assets/img/gen/articles/reduce-friction-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/reduce-friction-389.avif"
    alt="Reducing friction, Dall-E 3"
    loading="lazy">
  <figcaption><em>Reducing friction</em>, Dall-E 3</figcaption>
</figure>
<p>Finally, ‘knowing the developers’ means <em>helping the developers know</em>. Supporting their daily workflow by investing in writing rich documentation, templates and developer tooling, and spending time evangelizing the system, will reduce the friction of working on the system, and pay dividends in the long run.</p>
<p>You want both existing and future team members to feel intuitively familiar going into the codebase. This will allow them to make robust changes from day one.</p>
<h3>Principle 2: Keep It Simple</h3>
<p>Designing for longevity might sound like advocating for highly abstracted <a tabindex="0" href="https://wiki.c2.com/?RavioliCode">Ravioli Code</a>, but that is not the case. For instance, in the early phases of development, you can safely assume that each iteration will be so different that any abstraction you built in the previous one will have to be scrapped anyway. The pragmatic way to design for such drastic change is to cobble together an MVP with bash scripts and bubble gum, since the next version will start from scratch either way.</p>
<blockquote>
<p>Any complex system that works is invariably found to have evolved from a simple system that worked. The inverse proposition also appears to be true: A complex system designed from scratch never works and cannot be made to work. You have to start over, beginning with a working simple system.</p>
<p class="source"><strong>Gall’s Law</strong></p>
</blockquote>
<p>As a rule, abstractions should be avoided until they can pay for themselves by taming parts of the code which have become unwieldy. Pragmatic, well-timed abstraction is beneficial, whereas premature abstraction violates <a tabindex="0" href="http://principles-wiki.net/principles:gall_s_law">Gall’s Law</a> and leads to broken software.</p>
<p>Start Simple. <a tabindex="0" href="https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it">You Ain’t Gonna Need It</a>.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/matryoshka-doll-guy-642.avif 642w,
    /assets/img/gen/articles/matryoshka-doll-guy-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/matryoshka-doll-guy-389.avif"
    alt="But some day we might *need* six extra layers of abstraction..., Dall-E 3"
    loading="lazy">
  <figcaption><em>But some day we might *need* six extra layers of abstraction...</em>, Dall-E 3</figcaption>
</figure>
<h3>Principle 3: Design for change</h3>
<p>Try to ensure that the systems you design are <em>easy to change</em>.</p>
<h4>Naming things</h4>
<p>Make the code - and repo structure - intuitive, adhering to the <a tabindex="0" href="https://en.wikipedia.org/wiki/Principle_of_least_astonishment">Principle of Least Surprise</a>. If a developer doesn’t find the natural place to apply a change in the first or second place they look, they might just come up with a <em>creative solution</em> instead. Now you have two places where authorization rules are defined. Next week it could be three. Every ‘surprise’ in your design will attract code rot over time.</p>
<h4>Encapsulation</h4>
<p>Identify natural domain boundaries and encapsulate them with clear contracts. Rather than a technical boundary defined by microservice gateways and deployment bundles, or an organizational boundary defined by lines of management, a ‘domain boundary’ encapsulates a family of concepts which are likely to change together because they are intrinsically coupled, such as Users and UserProfiles.</p>
<p>Encapsulation by good domain boundaries simplifies future refactoring and aligns with real world aspects of your business/product, which are naturally long-lived.</p>
<h4>The paved road</h4>
<p>Prefer the well established stack over the ‘best’ one, since the 75% ‘batteries included’ solution that just works beats the 100% solution that requires constant tinkering because the stack is unstable and the developer ecosystem doesn’t exist yet. The exception is when you’re building systems at <em>massive scale</em>, where the absolute value of even a small incremental improvement can dramatically outweigh the cost of a team tinkering away on the 100% solution.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/yak-shaving-642.avif 642w,
    /assets/img/gen/articles/yak-shaving-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/yak-shaving-389.avif"
    alt="It may look like shaving a yak, but we're actually patching the ORM, Dall-E 3"
    loading="lazy">
  <figcaption><em>It may look like shaving a yak, but we're actually patching the ORM</em>, Dall-E 3</figcaption>
</figure>
<p>For open source dependencies, consider the size of the community and the number of contributors. If the project is maintained by a single person, it makes for a risky dependency. How frequently are major versions released? That is how often breaking changes are introduced. Do the maintainers keep updating and patching older versions, or will your team be forced to migrate to whole new versions every year to address unpatched security vulnerabilities?</p>
<p>When it comes to cloud platforms and application frameworks, these are load-bearing structures for your application, and you should choose the most stable and well supported option available. When it comes to libraries and services, you can afford to be more experimental as long as you take the necessary precautions, which brings us to…</p>
<h4>Loose coupling</h4>
<p>Keep systems loosely coupled: if you have fifty services all sending their logs to some 3rd party analytics tool, then consider creating a custom facade library to wrap all communication with the 3rd party tool. That way, when the day comes that the company changes logging tools (a safe bet), you only have to change the facade library in one place rather than all fifty services.</p>
<p>…but be pragmatic: If you can’t achieve looser coupling without adding a ton of complexity (stomping all over the <a tabindex="0" href="https://people.apache.org/~fhanik/kiss.html">KISS principle</a>), you are probably better off accepting a bit of tight coupling. Certain key decisions, such as your choice of cloud provider, are <a tabindex="0" href="https://www.inc.com/jeff-haden/amazon-founder-jeff-bezos-this-is-how-successful-people-make-such-smart-decisions.html">one way doors</a> with no easy way out, and while this may feel risky, it can bring great benefits; and one should never trade ten birds in the hand for one in the bush.</p>
<h4>Open standards</h4>
<p>Make sure that all critical business data is stored and accessible over open or de facto standards. ODBC is an open standard, as is Apache Arrow, whereas AWS S3 is an example of a defacto standard.</p>
<figure>
  <img class="blend-mode-luminosity"
    srcset="/assets/img/gen/articles/data-lock-in-pay-pay-pay-642.avif 642w,
    /assets/img/gen/articles/data-lock-in-pay-pay-pay-389.avif 389w"
    sizes="(min-width: 720px) 647px, calc(95.5vw - 28px)"
    src="https://jensroland.com/assets/img/gen/articles/data-lock-in-pay-pay-pay-389.avif"
    alt="Modern Data Warehousing: Extract-Load-Pay-Pay-Pay..., Dall-E 3"
    loading="lazy">
  <figcaption><em>Modern Data Warehousing: Extract-Load-Pay-Pay-Pay...</em>, Dall-E 3</figcaption>
</figure>
<p>Stay away from closed proprietary data formats to prevent data lock-in and maximise the chances of future tool integrations <em>just working</em> out of the box.</p>
<h2>Go build things to last</h2>
<blockquote>
<p><em>“What are you waiting for?! DO IT! JUST DO IT! YES, YOU CAN! JUST DO IT! If you’re tired of starting over, STOP GIVING UP!”</em></p>
<p class="source">~ <a tabindex="0" href="https://youtu.be/ZXsQAXx_ao0?si=elYi969rBnFawre4&amp;t=36">Shia LaBeouf</a>, possibly referring to this article</p>
</blockquote>
<p>Once you begin thinking of code in terms of longevity, it changes your approach to most aspects of software development.</p>
<p>The value of good documentation skyrockets when code needs to outlive multiple teams of maintainers. You still take on tech debt, but you may determine that some classes of tech debt (e.g. a quick and dirty one-off script or MVP) are acceptable while others (unintuitive naming or convention-breaking folder structures) are not. Your policies and preconceptions about automated tests, encapsulation, etc. – all need to be revisited when designing systems to stay in production for a decade or longer.</p>
<p>In return for this, however, you get a codebase that actively resists bad code; that neatly allows for innovation or changing requirements; a codebase that stays lean while maintaining high developer productivity for years – and which doesn’t require a costly bottom-up rebuild for every 2-3 years.</p>
<p>The result is faster development, higher quality, happier developers, and all of that at a significantly lower total cost of ownership.</p>
]]></content:encoded>
      <pubDate>Sun, 21 Apr 2024 00:00:00 +0200</pubDate>
      <guid isPermaLink="true">https://jensroland.com/articles/213/the-codebase-that-lasts-twice-as-long-costs-half-as-much</guid>
      <enclosure url="https://jensroland.com/assets/img/gen/covers/a-stable-platform-for-technology-large.avif" type="image/avif" length="0" />
      <category>software engineering</category>
      <category>architecture</category>
      <category>tech debt</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Start with a Monolith: a startup manifesto</title>
      <link>https://jensroland.com/articles/151/start-with-a-monolith-a-startup-manifesto</link>
      <description>A short, opinionated poem on entrepreneurship. Aggressively self-indulgent, pompous even... but it&apos;s not wrong.</description>
      <content:encoded><![CDATA[<p>I was going to qualify this by claiming I wrote it after binge watching <em>Silicon Valley</em>, but the truth is I’ve spent a good portion of my life thinking about - and living - the mistakes businesses keep making in the realm of software, product engineering, and entrepreneurship, and I’ve been wanting to distill some of it into a short, and most importantly, actionable format.</p>
<p>Something I could one day nail to my wall if and when I ever get around to launching a startup of my own.</p>
<p>I didn’t expect it to come out as poetry though.</p>
<blockquote>
<p>Start with a monolith</p>
<p>Start in the cloud</p>
<p>Start with a stack you know</p>
<p>Lean in for the KISS</p>
<p> </p>
<p>DevOps is rocket fuel</p>
<p>Tests give you speed</p>
<p>Stay off the bleeding edge</p>
<p>This is your creed</p>
<p> </p>
<p>Start with an MVP</p>
<p>Forget about scale</p>
<p>Look for that market fit</p>
<p>Make that first sale</p>
<p> </p>
<p>Obsess over customers</p>
<p>Know why they click</p>
<p>Shorten your feedback loop</p>
<p>Don’t be a dick</p>
<p> </p>
<p>Don’t chase that VC yet</p>
<p>Start with no dough</p>
<p>Launch early and often</p>
<p>Then pitch once you grow</p>
<p> </p>
<p>And when you have…</p>
<p> </p>
<p>Your culture can get out of hand</p>
<p>like a manifesto turned poem</p>
<p>Don’t hire what you cannot coach</p>
<p>Grow slow… or grow home</p>
</blockquote>
<p>I will free admit that this is aggressively self-indulgent, pompous even… but it’s <em>not wrong</em>.</p>
]]></content:encoded>
      <pubDate>Wed, 28 Sep 2022 00:00:00 +0200</pubDate>
      <guid isPermaLink="true">https://jensroland.com/articles/151/start-with-a-monolith-a-startup-manifesto</guid>
      <enclosure url="https://jensroland.com/assets/img/gen/covers/pexels-pixabay-161798.avif" type="image/avif" length="0" />
      <category>leadership</category>
      <category>lean</category>
      <category>software engineering</category>
      <category>entrepreneurship</category>
    </item>
    <item>
      <title>On Leadership</title>
      <link>https://jensroland.com/articles/116/on-leadership</link>
      <description>Hiring the right people, giving them what they need, and getting out of the way.</description>
      <content:encoded><![CDATA[<p>My leadership style boils down to three deceptively simple practices: <em>hiring the right people, giving them what they need, and getting out of the way</em>.</p>
<h2>Hiring the right people</h2>
<p>If I am going hiking, I don’t want the person who has memorized all the different hardening grades in the alloys of his pocket knife. I want the person who could <em>start a fire in the dark in four different ways with nothing but a rock and pinecone. And in two more ways if the pinecone is wet</em>.</p>
<p>Apply that to software and you have a surprisingly powerful recruiting method. Personally, in 10 years of hiring, I have not regretted a single hiring decision I made in this way.</p>
<p>(This is not the article to get into the details of hiring developers, but if you are interested, feel free to ask me. You may regret asking though – I can talk for <em>hours</em> about recruiting.)</p>
<h2>Giving them what they need</h2>
<p>Par for the course is powerful hardware, choose-your-own-keyboard, and the occasional conference ticket; but two essential things that top developers need to be both happy and highly productive, is <strong>clear direction</strong> and <strong>trust</strong>.</p>
<p>Once developers understand the “why” behind a high level requirement and are trusted with the details, they are free to explore and implement new and creative solutions. Such solutions may arrive at the goal in unconventional ways, but they almost always arrive both faster and better than anticipated by stakeholders or architects.</p>
<h2>Getting out of the way</h2>
<p>Obviously, don’t micromanage, but take it a step further and <em>eliminate any and all interruptions and dependencies</em> that get in the way of the work or pull the developer out of their productive ‘flow’. By my count, even the smallest interruption costs at least 30 minutes of productivity, in part due to the context switch itself but mainly from the process of slowly getting back into the flow of coding.</p>
<p>As for dependencies, any process that requires getting a response from a person outside the immediate team introduces a perfect storm of context switching, busy-waiting, and in some case even anxiety; these are people who may very well have spent 10,000 hours learning to code alone in front of a computer: a lot of them don’t like talking to people, let alone people they don’t know.</p>
<p>Finally, hands-off management is not ears-off management. Every team has needs which evolve over time, and as a leader your job is to listen and provide, whether that means coaching them, buying them nerf guns, or fighting off stakeholders when they come bearing gifts of this week’s feature creep (and it is <em>always</em> feature creep).</p>
]]></content:encoded>
      <pubDate>Wed, 09 Mar 2022 00:00:00 +0100</pubDate>
      <guid isPermaLink="true">https://jensroland.com/articles/116/on-leadership</guid>
      <enclosure url="https://jensroland.com/assets/img/gen/covers/pexels-cottonbro-studio-4065145.avif" type="image/avif" length="0" />
      <category>leadership</category>
      <category>recruiting</category>
      <category>software engineering</category>
    </item>
    <item>
      <title>On Developer Productivity</title>
      <link>https://jensroland.com/articles/114/on-developer-productivity</link>
      <description>About no-blame culture, uninterrupted flow, and high trust in developer teams.</description>
      <content:encoded><![CDATA[<p>In my experience, software development velocity correlates strongly with:</p>
<ol>
<li>The willingness to make mistakes. Small mistakes can be accepted as the cost of doing business, bigger ones can be automatically detected and remediated in real time. Velocity is gained through automation and no-blame culture.</li>
<li>The number of hours per week where a state of high-productivity <em>‘flow’</em> can be achieved. Flow can only happen where developers have uninterrupted time and clarity of goals. Velocity is gained through hands-off leadership.</li>
<li>The ability to <em>make decisions in real time</em>, without having to ask permission or coordinate with external teams. Velocity is gained through trust, transparency and architectural decoupling.</li>
</ol>
<p><strong>In short: Automation, No-blame culture, Hands-off leadership, Trust, Transparency, and Architectural decoupling.</strong></p>
<p>Tenets of Lean, but boogeymen in the typical enterprise.</p>
]]></content:encoded>
      <pubDate>Tue, 08 Mar 2022 00:00:00 +0100</pubDate>
      <guid isPermaLink="true">https://jensroland.com/articles/114/on-developer-productivity</guid>
      <enclosure url="https://jensroland.com/assets/img/gen/covers/pexels-thisisengineering-3861958.avif" type="image/avif" length="0" />
      <category>leadership</category>
      <category>productivity</category>
      <category>software engineering</category>
      <category>lean</category>
    </item>
  </channel>
</rss>