<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://slavikdev.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://slavikdev.com/" rel="alternate" type="text/html" /><updated>2026-03-15T12:13:20+00:00</updated><id>https://slavikdev.com/feed.xml</id><title type="html">Engineering Management &amp;amp; Tech Leadership Blog | Slavik Shynkarenko</title><subtitle>Insights on software engineering, platform engineering, and leadership from experienced manager Slavik Shynkarenko. Learn and grow today!
</subtitle><entry><title type="html">Do We Need Unit Tests in 2026?</title><link href="https://slavikdev.com/do-we-need-unit-tests/" rel="alternate" type="text/html" title="Do We Need Unit Tests in 2026?" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://slavikdev.com/do-we-need-unit-tests</id><content type="html" xml:base="https://slavikdev.com/do-we-need-unit-tests/"><![CDATA[<p>Back in 2019, I wrote the original version of this article. The arguments against unit tests were familiar: <em>they don’t hit real dependencies, 100% coverage means nothing, passing tests don’t guarantee working software.</em> All fair points. But the world has changed drastically since then.</p>

<p>AI coding assistants now generate thousands of lines of code per day. Engineers ship faster than ever. And the question “do we need unit tests?” has taken on an entirely different dimension — because the entity writing your code isn’t always human anymore.</p>

<p>My answer, seven years later, is more emphatic than before: <strong>yes, you need unit tests. And in the AI era, you need them more than ever.</strong></p>

<h2 id="the-classic-arguments-still-standing">The Classic Arguments (Still Standing)</h2>

<p>Let me briefly revisit the original case, because the fundamentals haven’t changed.</p>

<p>Consider a system composed of three simple operations:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">S</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">mul</span><span class="p">(</span><span class="nx">sum</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">),</span> <span class="nx">sub</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">))</span>
</code></pre></div></div>

<p>where:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">sum</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
<span class="nx">sub</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">-</span> <span class="nx">b</span>
<span class="nx">mul</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">*</span> <span class="nx">b</span>
</code></pre></div></div>

<p>High-level tests check the final output:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">S</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>   <span class="c1">// (0 + 0) * (0 - 0) = 0</span>
<span class="nx">S</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span>  <span class="c1">// (0 + 1) * (0 - 1) = -1</span>
<span class="nx">S</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>   <span class="c1">// (1 + 1) * (1 - 1) = 0</span>
</code></pre></div></div>

<p>Now introduce a bug — change <code class="language-plaintext highlighter-rouge">sum(a, b)</code> from <code class="language-plaintext highlighter-rouge">a + b</code> to <code class="language-plaintext highlighter-rouge">a + 1</code>. Every system test still passes:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">S</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>   <span class="c1">// (0 + 1) * (0 - 0) = 0</span>
<span class="nx">S</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span>  <span class="c1">// (0 + 1) * (0 - 1) = -1</span>
<span class="nx">S</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>   <span class="c1">// (1 + 1) * (1 - 1) = 0</span>
</code></pre></div></div>

<p>The bug is invisible at the system level because the test inputs happen to mask it. A unit test for <code class="language-plaintext highlighter-rouge">sum</code> would catch it instantly. This principle scales: the more units you have, the more likely a high-level test will miss a localized defect. Real systems have thousands of units. A 1:1000 ratio between system tests and code paths is generous.</p>

<p>This was true in 2019. It’s true in 2026. The math doesn’t care about technology trends.</p>

<h2 id="what-changed-ai-writes-code-humans-verify-it">What Changed: AI Writes Code, Humans Verify It</h2>

<p>Here’s what’s fundamentally different now. In 2019, humans wrote both the code and the tests. The common complaint was that writing unit tests doubled the workload. <em>”I could be shipping features instead of writing test cases for obvious logic.”</em></p>

<p>That argument is dead.</p>

<p>AI coding assistants — Claude Code, GitHub Copilot, Cursor — generate code at a pace no human can match. They also generate unit tests. The cost of writing tests has dropped to near zero. What used to take an afternoon now takes minutes. You describe the behavior you want, and the AI scaffolds comprehensive test suites that cover happy paths, edge cases, and error conditions.</p>

<p>But here’s the twist that most people miss: <strong>the lower cost of generating code makes verification more important, not less.</strong></p>

<p>When a human writes a function, they hold the full context in their head — the business requirement, the edge cases they considered, the trade-offs they made. When an AI generates a function, it’s making statistical predictions about what code should look like given its training data. It’s usually right. But “usually” isn’t a word you want in production.</p>

<p>I’ve seen AI-generated code that looked perfectly clean, passed a code review, and hid a subtle off-by-one error deep in a loop condition. I’ve seen it produce correct logic for 99% of inputs and silently corrupt data for a specific combination of null and empty string. These aren’t hypothetical scenarios — they’re Tuesday.</p>

<p>Unit tests are the verification layer for AI-generated code. They’re the contract that says: <em>regardless of who or what wrote this function, here’s what it must do.</em> Without them, you’re trusting a probabilistic system to be deterministic. That’s not engineering — that’s gambling.</p>

<h2 id="the-tests-are-documentation-argument-got-stronger">The “Tests Are Documentation” Argument Got Stronger</h2>

<p>In 2019, I argued that unit tests serve as documentation. That was true but somewhat academic — most teams had developers who knew the codebase intimately enough that reading tests for understanding was optional.</p>

<p>In 2026, the documentation argument is critical. Here’s why.</p>

<p>AI-assisted codebases grow faster than any team’s ability to hold them in their heads. Engineers context-switch between multiple services, often inheriting code that an AI generated in a previous sprint — or that a colleague generated with an AI and didn’t fully review. The codebase is increasingly <em>nobody’s code.</em></p>

<p>When you pick up a function you’ve never seen and need to understand what it does, what do you reach for? The implementation might be 50 lines of dense logic. The commit message says “Add processing pipeline.” The PR description is AI-generated boilerplate.</p>

<p>But the tests? The tests say:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">returns empty array when input is null</span><span class="dl">'</span><span class="p">,</span> <span class="p">...)</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">excludes items with negative quantity</span><span class="dl">'</span><span class="p">,</span> <span class="p">...)</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">applies discount before tax calculation</span><span class="dl">'</span><span class="p">,</span> <span class="p">...)</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">throws when discount exceeds item price</span><span class="dl">'</span><span class="p">,</span> <span class="p">...)</span>
</code></pre></div></div>

<p>That’s a specification. It tells you exactly what the author intended — edge cases included. In a world where you can’t always ask the original author because the original author was an LLM, tests are the closest thing you have to a source of truth.</p>

<h2 id="when-unit-tests-genuinely-dont-help">When Unit Tests Genuinely Don’t Help</h2>

<p>I’m not a unit test absolutist. There are situations where they provide little value, and pretending otherwise makes the whole practice feel like cargo cult engineering.</p>

<p><strong>Thin wrappers and pass-through code.</strong> If a function does nothing but call another function and return its result, testing it is testing the language runtime. Skip it.</p>

<p><strong>UI layout and styling.</strong> Does this button render 20 pixels from the left edge? That’s a visual regression test, not a unit test. Tools like Playwright and Storybook handle this far better.</p>

<p><strong>Glue code in rapidly changing prototypes.</strong> If you’re in discovery mode and the code will be rewritten three times in the next two weeks, investing in comprehensive unit tests is waste. Write a few smoke tests, move fast, and add proper tests when the design stabilizes.</p>

<p><strong>Pure integration logic.</strong> Sometimes the interesting behavior <em>is</em> the interaction between components — database queries, API calls, message flows. Unit-testing these with mocks can create a false sense of security. If your mock says the database returns <code class="language-plaintext highlighter-rouge">[{id: 1}]</code> but the real query returns <code class="language-plaintext highlighter-rouge">[{id: 1, deleted_at: null}]</code> with an extra field that breaks your deserializer, the mock won’t save you. This is where integration tests and contract tests earn their keep.</p>

<p>The key insight: unit tests excel at verifying <em>logic</em> — branching, calculations, transformations, state machines. They’re weak at verifying <em>interactions.</em> Know the difference and test accordingly.</p>

<h2 id="the-testing-pyramid-is-dead-long-live-the-testing-diamond">The Testing Pyramid Is Dead. Long Live the Testing Diamond.</h2>

<p>The traditional testing pyramid — many unit tests at the base, fewer integration tests in the middle, a handful of E2E tests at the top — was designed for a world where tests were expensive to write and slow to run.</p>

<p>Both constraints have loosened dramatically.</p>

<p>AI generates tests at every level almost equally fast. And modern tooling has made integration tests much faster than they used to be. Testcontainers spins up a real PostgreSQL instance in under two seconds. Playwright runs a full browser flow in milliseconds. The infrastructure excuse for avoiding higher-level tests barely holds anymore.</p>

<p>What’s emerging in practice is more of a diamond shape:</p>

<ul>
  <li><strong>A small base of unit tests</strong> for complex, algorithmic logic — parsers, validators, state machines, financial calculations. Things with many branches and edge cases.</li>
  <li><strong>A thick middle of integration and component tests</strong> that verify real interactions between real components. API endpoint tests that hit a real database. Service tests that use real queues. This is where the highest ROI lives for most web applications.</li>
  <li><strong>A thin top of E2E tests</strong> for critical user journeys. Checkout flow. Login. The paths that, if broken, wake someone up at 3 AM.</li>
</ul>

<p>This isn’t a rejection of unit tests. It’s a recognition that not all code benefits equally from them. The business logic layer with complex branching? Unit test it heavily. The CRUD controller that maps an HTTP request to a database query? An integration test is more honest about what can actually go wrong.</p>

<h2 id="mutation-testing-proving-your-tests-actually-work">Mutation Testing: Proving Your Tests Actually Work</h2>

<p>Here’s a practice that was niche in 2019 and deserves far more attention in 2026: mutation testing.</p>

<p>The idea is simple. A mutation testing tool takes your code, introduces small changes (mutations) — flipping a <code class="language-plaintext highlighter-rouge">&gt;</code> to <code class="language-plaintext highlighter-rouge">&gt;=</code>, removing a <code class="language-plaintext highlighter-rouge">return</code> statement, replacing <code class="language-plaintext highlighter-rouge">true</code> with <code class="language-plaintext highlighter-rouge">false</code> — and runs your test suite against each mutant. If your tests still pass after a mutation, they’re not actually testing that code path. The mutant “survived,” and your test suite has a blind spot.</p>

<p>Coverage metrics tell you which lines your tests execute. Mutation testing tells you which lines your tests actually <em>verify.</em> Those are very different things.</p>

<p>Tools like Stryker (JavaScript/TypeScript), mutmut (Python), and pitest (Java) are mature and practical. Running them on your most critical modules — even quarterly — reveals exactly where your test suite is theatrical rather than useful.</p>

<p>This matters especially for AI-generated tests. An LLM will happily produce a test suite with 95% line coverage that catches almost nothing, because it optimizes for the superficial pattern of “code that looks like tests” rather than the deeper pattern of “assertions that constrain behavior.” Mutation testing catches this.</p>

<h2 id="tdd-in-the-age-of-ai">TDD in the Age of AI</h2>

<p>Test-driven development — write the test first, watch it fail, make it pass — was already controversial in 2019. In 2026, it’s been reshaped but not replaced.</p>

<p>The classic TDD cycle assumed a human writing both tests and implementation, alternating in tight loops. With AI, a more effective pattern has emerged: <strong>specification-driven generation.</strong></p>

<p>You write the tests first — or more precisely, you write the <em>specifications</em> as tests. What should this function accept? What should it return? What are the error cases? Then you hand those tests to an AI and say: <em>make these pass.</em></p>

<p>This is TDD on steroids. The AI generates an implementation that satisfies your contracts, and you review it. If the implementation looks wrong despite passing tests, your tests are incomplete — add more. If it looks right, you’ve just built a verified component in minutes instead of hours.</p>

<p>The human remains the designer. The AI is the builder. The tests are the blueprint. This workflow only works if the tests exist <em>before</em> the implementation. Otherwise, you’re back to generating code and then generating tests that confirm what the code already does — which is circular and catches nothing.</p>

<h2 id="what-id-tell-my-2019-self">What I’d Tell My 2019 Self</h2>

<p>Seven years ago, I ended the original article by arguing that unit tests serve as documentation and that TDD helps maintain low coupling. Both still true. But I was too defensive about it. I spent too much energy justifying tests at all, rather than talking about <em>how</em> to test well.</p>

<p>Here’s what I know now that I didn’t know then:</p>

<p><strong>The cost of writing tests is no longer the bottleneck.</strong> AI eliminated it. The cost is now in <em>designing the right tests</em> — choosing what to assert, which edge cases matter, where to draw the boundary between unit and integration.</p>

<p><strong>Coverage is a vanity metric. Mutation score is a quality metric.</strong> Chase the latter.</p>

<p><strong>Mocking is a design smell, not a testing strategy.</strong> If you need to mock ten dependencies to test one function, the function is doing too much. Fix the design, don’t duct-tape the test. The original article was right that coupling matters — but I’d go further now: excessive mocking is a sign you’re testing the wrong way, not that you need more mocks.</p>

<p><strong>The best test suite is the one your team actually runs.</strong> Speed matters. Flaky tests get ignored. A fast, reliable suite of 200 tests beats a comprehensive but flaky suite of 2,000 every time.</p>

<p><strong>Write tests for the code that scares you.</strong> The function with six nested conditions? Test it. The billing calculation? Test it. The straightforward getter? Don’t.</p>

<p>Unit tests aren’t about proving your code is correct. They never were. They’re about catching the moment it stops being correct — whether the change came from a human, an AI, or a 3 AM hotfix pushed by someone who “just needed to fix one thing.”</p>

<p>In the AI era, that catching matters more than ever. Write the tests. Let the machines write the code.</p>]]></content><author><name></name></author><category term="unit-tests" /><category term="testing" /><category term="ai-coding" /><category term="tdd" /><category term="software-engineering" /><summary type="html"><![CDATA[Unit tests matter more than ever in the AI era. AI generates code fast but not always correctly. Here is why and how to test in 2026.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/do-we-need-unit-tests.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467,%20%22author%22=%3E%22Jeremy%20Thomas%22,%20%22source%22=%3E%22Unsplash%22%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/do-we-need-unit-tests.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467,%20%22author%22=%3E%22Jeremy%20Thomas%22,%20%22source%22=%3E%22Unsplash%22%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How Miri’s Compiler Works: From Source Code to Native Binary</title><link href="https://slavikdev.com/how-does-miri-compiler-work/" rel="alternate" type="text/html" title="How Miri’s Compiler Works: From Source Code to Native Binary" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://slavikdev.com/how-does-miri-compiler-work</id><content type="html" xml:base="https://slavikdev.com/how-does-miri-compiler-work/"><![CDATA[<p>I spent years convinced that compilers were dark magic — something only wizards at Mozilla, Microsoft, or Google built. Then I decided to write one anyway. Turns out, the transformation from human-readable text to executable binary isn’t magic at all. It’s a pipeline — six phases, each doing one thing well, each handing its output to the next.</p>

<p>What follows is how Miri’s compiler actually works: six phases that turn a <code class="language-plaintext highlighter-rouge">.mi</code> source file into a native executable. You don’t need a compiler background to follow along. In fact, that’s kind of the point: compilers are less arcane than they seem, and the ideas behind them are elegant in a way that I think every engineer can appreciate.</p>

<h2 id="six-phases-one-job-each">Six phases, one job each</h2>

<p>When you type <code class="language-plaintext highlighter-rouge">miri run hello.mi</code>, here’s what actually happens:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Source Code (.mi)
     |
     v
   Lexer -----&gt; Token Stream
     |
     v
   Parser ----&gt; Abstract Syntax Tree (AST)
     |
     v
Type Checker -&gt; Validated AST + Type Info
     |
     v
MIR Lowering -&gt; Basic Blocks + Control Flow Graph
     |
     v
  Codegen ----&gt; Native Machine Code (via Cranelift)
     |
     v
  Linker -----&gt; Executable Binary
</code></pre></div></div>

<p>Each phase has one job: take the output of the previous phase, transform it, and hand it to the next one. The lexer doesn’t know about types. The type checker doesn’t know about machine code. This separation is what makes the whole thing manageable — you can think about one problem at a time, and each layer is independently testable.</p>

<p>Let me take you through each phase.</p>

<h2 id="the-lexer-teaching-a-machine-to-read">The Lexer: teaching a machine to read</h2>

<p>The lexer is the simplest phase conceptually, and yet it’s where some of the trickiest edge cases live.</p>

<p>Its job is to turn raw characters into <strong>tokens</strong> — meaningful chunks that the next phase can work with. Think of it like reading a sentence. Your brain doesn’t process individual letters; it recognizes words, punctuation, spacing. The lexer does the same for code.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Source:   let x = 42 + 3
Tokens:   [Let] [Identifier("x")] [Assign] [Integer(42)] [Plus] [Integer(3)]
</code></pre></div></div>

<p>Miri’s lexer is built on top of the <a href="https://github.com/maciejhirsz/logos" class="underline">Logos</a> crate — very fast lexer generator for Rust. Logos handles the boring parts: matching keywords, recognizing operators, parsing number formats. But Miri adds a stateful layer on top that handles things Logos can’t.</p>

<p>The most interesting of those is <strong>indentation tracking</strong>. Like Python, Miri uses significant whitespace — indentation determines code blocks, not curly braces. This means the lexer has to emit synthetic <code class="language-plaintext highlighter-rouge">Indent</code> and <code class="language-plaintext highlighter-rouge">Dedent</code> tokens whenever the indentation level changes:</p>

<pre><code class="language-miri">if x &gt; 0                     # indent_stack: [0]
    println("positive")      # indent_stack: [0, 4] -&gt; emit Indent
    println("and nonzero")
println("always runs")       # indent_stack: [0]    -&gt; emit Dedent
</code></pre>

<p>The lexer maintains a stack of indentation levels and compares each new line against the top of the stack. More indentation? Push and emit <code class="language-plaintext highlighter-rouge">Indent</code>. Less? Pop (possibly multiple times) and emit <code class="language-plaintext highlighter-rouge">Dedent</code> for each level unwound. Same? Just a regular statement boundary.</p>

<p>There’s an important exception here: indentation changes are <strong>suppressed</strong> inside parentheses, brackets, and curly braces. Otherwise, you couldn’t write multi-line function calls or collection literals without the lexer panicking about inconsistent indentation. The lexer tracks bracket nesting depth and simply ignores whitespace changes when any bracket counter is above zero.</p>

<p>Another subtle problem the lexer solves: when it sees <code class="language-plaintext highlighter-rouge">5.</code>, that could be the start of a float (<code class="language-plaintext highlighter-rouge">5.0</code>), a range operator (<code class="language-plaintext highlighter-rouge">5..10</code>), or a method call on an integer (<code class="language-plaintext highlighter-rouge">5.method</code>). Logos initially matches this as an ambiguous <code class="language-plaintext highlighter-rouge">FloatOrRange</code> token, and then Miri’s stateful layer peeks at the next character to figure out which one it is. These kinds of disambiguation decisions — where one character of lookahead changes the entire meaning — are what make lexers more interesting than they first appear.</p>

<h2 id="the-parser-where-structure-emerges">The Parser: where structure emerges</h2>

<p>The parser takes the flat stream of tokens and builds a tree — the <strong>Abstract Syntax Tree</strong>, or AST. This is where structure emerges. The AST represents the hierarchical relationships in your code: this expression is the condition of that <code class="language-plaintext highlighter-rouge">if</code> statement, this <code class="language-plaintext highlighter-rouge">+</code> operator has these two operands, this function has these parameters and this body.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Tokens:   [Let] [x] [=] [1] [+] [2] [*] [3]

AST:            VariableDeclaration
                ├── name: "x"
                └── init: Binary(+)
                      ├── left: Literal(1)
                      └── right: Binary(*)
                            ├── left: Literal(2)
                            └── right: Literal(3)
</code></pre></div></div>

<p>Notice that <code class="language-plaintext highlighter-rouge">*</code> binds tighter than <code class="language-plaintext highlighter-rouge">+</code> — the tree correctly represents <code class="language-plaintext highlighter-rouge">1 + (2 * 3)</code>, not <code class="language-plaintext highlighter-rouge">(1 + 2) * 3</code>. Getting operator precedence right is one of the parser’s core responsibilities.</p>

<p>Miri uses a hybrid parsing strategy. For statements and declarations — <code class="language-plaintext highlighter-rouge">if</code>, <code class="language-plaintext highlighter-rouge">for</code>, <code class="language-plaintext highlighter-rouge">fn</code>, <code class="language-plaintext highlighter-rouge">class</code> — it uses <strong>recursive descent</strong>: each grammar rule is a function, and the functions call each other based on what token they see. This is the most intuitive approach; the code structure mirrors the grammar.</p>

<p>For expressions with operators, Miri uses <strong>Pratt parsing</strong> (also called precedence climbing). The idea is elegant: each precedence level has a function that parses the operand at the next-higher level, then loops while it sees an operator at its own level, combining left and right into a binary node. Twelve precedence levels — from <code class="language-plaintext highlighter-rouge">or</code> at the lowest to primary literals at the highest — chain together naturally, and the whole thing handles complex expressions like <code class="language-plaintext highlighter-rouge">a + b * c == d and e or f</code> without any ambiguity.</p>

<p>The parser only needs <strong>one token of lookahead</strong> to decide what to do. It peeks at the next token without consuming it: see <code class="language-plaintext highlighter-rouge">if</code>? Parse an if-statement. See <code class="language-plaintext highlighter-rouge">fn</code>? Parse a function declaration. See a number? Start parsing an expression. This simplicity — one token is enough — is a sign that the grammar is well-designed.</p>

<p>One design choice I like about Miri’s parser: every AST node gets a <strong>unique sequential ID</strong>, generated by an atomic counter. The type checker later uses these IDs to store inferred types in a separate <code class="language-plaintext highlighter-rouge">HashMap&lt;NodeId, Type&gt;</code>, without ever modifying the AST. This keeps the AST immutable after construction — a small decision that prevents an entire category of subtle bugs.</p>

<h2 id="the-type-checker-catching-mistakes-before-they-happen">The Type Checker: catching mistakes before they happen</h2>

<p>The parser validates syntax — <em>is the code structured correctly?</em> — but it doesn’t check whether the code makes logical sense. That’s the type checker’s job.</p>

<pre><code class="language-miri">let x = 42
let y = "hello"
let z = x + y       # Type error: can’t add Int and String
</code></pre>

<p>Without type checking, this would blow up at runtime. With it, the error is caught before the program ever runs.</p>

<p>Miri’s type checker uses a <strong>two-pass architecture</strong>, and the reason is simple. Consider this perfectly valid code:</p>

<pre><code class="language-miri">fn main()
    greet("Alice")    # greet() hasn’t been defined yet!

fn greet(name String)
    println(f"Hello, {name}")
</code></pre>

<p>In a single pass, the call to <code class="language-plaintext highlighter-rouge">greet()</code> would fail because the function hasn’t been seen yet. So Pass 1 walks the entire program and registers all function signatures, class definitions, struct definitions, and type hierarchies. Pass 2 then checks everything else — function bodies, expressions, type compatibility — with full knowledge of what exists.</p>

<p>The type checker supports <strong>type inference</strong>, which means you don’t have to annotate everything:</p>

<pre><code class="language-miri">let x = 42              # Inferred: Int
let name = "Alice"      # Inferred: String
let items = [1, 2, 3]   # Inferred: [Int]
</code></pre>

<p>For collections, it infers the element type from the first element, then verifies all other elements match. For function calls, it looks up the function’s declared return type. For generics, it infers type parameters from the arguments you pass — <code class="language-plaintext highlighter-rouge">identity(42)</code> infers <code class="language-plaintext highlighter-rouge">T = Int</code> without you ever specifying it.</p>

<p>Type compatibility in Miri follows a decision tree with about ten steps: exact match, error suppression (to prevent cascading errors), option compatibility (<code class="language-plaintext highlighter-rouge">T</code> is compatible with <code class="language-plaintext highlighter-rouge">T?</code>), numeric widening (<code class="language-plaintext highlighter-rouge">i8</code> can be used where <code class="language-plaintext highlighter-rouge">i64</code> is expected, but not the reverse), subtype checking (class inheritance), and so on. The order matters — earlier checks are faster and more common, so the system short-circuits quickly.</p>

<p>One detail I’m particularly pleased with: the type checker reports <strong>multiple errors</strong> in a single pass. If your program has three type errors, you see all three at once, not just the first one. To make this work without drowning you in noise, it uses an <code class="language-plaintext highlighter-rouge">Error</code> type as a sentinel — once an expression fails type checking, any operation on that <code class="language-plaintext highlighter-rouge">Error</code> value is silently accepted. A single typo produces one error message, not twenty cascading ones.</p>

<h2 id="mir-flattening-the-world">MIR: flattening the world</h2>

<p>This is where things get really interesting. The AST is too high-level for code generation — it has nested <code class="language-plaintext highlighter-rouge">if/else</code>, <code class="language-plaintext highlighter-rouge">for</code> loops, <code class="language-plaintext highlighter-rouge">match</code> expressions, classes, all these rich constructs. Machine code understands none of that. It only knows: move a value, add two numbers, jump to an address.</p>

<p>MIR — the Mid-level Intermediate Representation — bridges the gap. It decomposes everything into <strong>basic blocks</strong>: straight-line sequences of simple instructions, each ending with a “terminator” that says where to go next.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// What you write:                // What MIR produces:
if x &gt; 0                         bb0:
    return x                        _2 = Gt(_1, 0)
else                                SwitchInt(_2) -&gt; [true: bb1, else: bb2]
    return -x
                                  bb1:
                                    _0 = Copy(_1)
                                    Return

                                  bb2:
                                    _0 = Neg(_1)
                                    Return
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">for</code> loop becomes a while-like pattern with an index variable and a bounds check. A <code class="language-plaintext highlighter-rouge">match</code> expression becomes a <code class="language-plaintext highlighter-rouge">SwitchInt</code> with branches. Method calls become mangled function calls like <code class="language-plaintext highlighter-rouge">String_to_upper</code>. Every high-level construct is “desugared” into these primitives.</p>

<p>After the initial lowering, MIR runs through <strong>optimization passes</strong> — constant propagation (replace <code class="language-plaintext highlighter-rouge">5 + 3</code> with <code class="language-plaintext highlighter-rouge">8</code> at compile time), copy propagation (eliminate redundant copies), dead code elimination (remove computations whose results are never used), and control flow simplification (thread chains of empty blocks). These passes iterate until nothing changes, up to ten rounds.</p>

<p>But the most important MIR pass isn’t an optimization in the traditional sense. It’s <strong>Perceus</strong> — automatic reference counting insertion — and it’s at the heart of how Miri manages memory. More on that in a moment.</p>

<h2 id="memory-management-why-i-bet-against-garbage-collection">Memory management: why I bet against garbage collection</h2>

<p>Every language has to answer the question: <em>who frees the memory?</em></p>

<p>In C, you do it manually — and get it wrong constantly. In Java and Go, a garbage collector does it for you — but it runs on its own schedule, introducing latency spikes that are murder for real-time systems. In Rust, the borrow checker enforces ownership rules at compile time — brilliant, but the learning curve is steep and the annotation burden is real.</p>

<p>I spent weeks going back and forth on this. GC would have been the path of least resistance — well-understood, easy to implement, handles cycles for free. And yes, you <em>could</em> run a GC on the CPU to manage GPU memory — it’s not technically impossible. But the more I thought about it, the worse the fit became.</p>

<p>GPU computing demands predictable memory lifetimes. You need to know <em>exactly</em> when a buffer is safe to reuse, when data can be transferred back, when GPU memory can be freed. A GC gives you none of that — it frees things eventually, on its own schedule, after a tracing pass that might pause the CPU and starve the GPU pipeline of work. And if Miri eventually compiles code that runs <em>on</em> the GPU, the kernel side would need a completely different memory strategy anyway — meaning two memory models in one language, which is exactly the kind of complexity I’m trying to avoid.</p>

<p>Reference counting sidesteps all of this. It’s deterministic, it’s per-object, and — critically — it works the same way on both CPU and GPU code paths. One memory model, one set of semantics, no special cases.</p>

<p>So Miri takes that path: <strong>automatic reference counting</strong>, inserted at compile time by the Perceus algorithm.</p>

<p>The idea is straightforward. Every heap-allocated object — strings, lists, maps, class instances — has a reference count: how many variables point to it. When you assign a list to a new variable, the count goes up. When a variable goes out of scope, the count goes down. When it hits zero, the memory is freed immediately.</p>

<pre><code class="language-miri">let a = [1, 2, 3]     # List allocated, RC = 1
let b = a              # RC = 2 (b aliases a)
# b goes out of scope  # RC = 1
# a goes out of scope  # RC = 0 -&gt; freed immediately
</code></pre>

<p>What makes Perceus special is that the <em>programmer never writes any of this</em>. The MIR pass analyzes every function and inserts <code class="language-plaintext highlighter-rouge">IncRef</code> and <code class="language-plaintext highlighter-rouge">DecRef</code> instructions automatically. When a managed value is copied, Perceus adds an <code class="language-plaintext highlighter-rouge">IncRef</code> before the copy. When a local goes out of scope, it adds a <code class="language-plaintext highlighter-rouge">DecRef</code>. Primitives and small structs (128 bytes or less, all primitive fields) are just copied by value — no reference counting overhead at all.</p>

<p>This gives Miri a distinctive position in the memory management landscape:</p>

<ul>
  <li><strong>vs. C/C++:</strong> No manual <code class="language-plaintext highlighter-rouge">malloc</code>/<code class="language-plaintext highlighter-rouge">free</code>, no dangling pointers, no double-frees. You write code and the compiler handles cleanup.</li>
  <li><strong>vs. Python/Java/Go:</strong> No garbage collector, no GC pauses, no unpredictable latency. Memory is freed deterministically, the instant it’s no longer needed.</li>
  <li><strong>vs. Rust:</strong> No borrow checker, no lifetime annotations, no fighting the compiler about references. The trade-off is that Miri’s approach has slightly more runtime overhead (those reference count increments and decrements aren’t free), and it can’t detect reference cycles automatically. But for the vast majority of programs, the ergonomics win is significant.</li>
  <li><strong>vs. Swift:</strong> Swift also uses automatic reference counting, but requires the programmer to think about <code class="language-plaintext highlighter-rouge">weak</code> and <code class="language-plaintext highlighter-rouge">unowned</code> references to break cycles. Miri’s Perceus approach aims to handle more of this automatically.</li>
</ul>

<p>The deterministic cleanup is particularly important for Miri’s long-term vision. When you’re coordinating memory across CPU and GPU, you need tight control over object lifetimes — not a GC that might pause at the worst possible moment. Reference counting gives you that: each object manages its own lifetime, cleanup is immediate and predictable, and the same IncRef/DecRef semantics work regardless of where the code runs.</p>

<h2 id="code-generation-the-final-transformation">Code generation: the final transformation</h2>

<p>MIR’s basic blocks become actual machine instructions. This is where the abstract finally becomes concrete.</p>

<p>Miri uses <strong>Cranelift</strong> as its code generation backend — a fast, lightweight code generator originally built for WebAssembly. Cranelift isn’t as aggressive at optimization as LLVM (the backend used by Rust, Clang, and Swift), but it compiles <em>much</em> faster. For a language still in alpha, where fast iteration on the compiler itself matters more than squeezing out every last CPU cycle, that’s the right trade-off.</p>

<p>The type mapping is simple by design: integers map to native integer types, floats to native float types, booleans to a single byte. <strong>Everything else is a pointer.</strong> Strings, lists, maps, structs, class instances — they all live on the heap, and the compiled code manipulates pointers to them.</p>

<p>Every heap-allocated object follows a uniform layout: a reference count stored just before the payload data. String literals are special — their reference count is set to a sentinel “immortal” value (the high bit is set), so they’re never freed. This is a small optimization that eliminates pointless reference counting on data that lives for the entire program.</p>

<p>After Cranelift produces an object file, the system linker (<code class="language-plaintext highlighter-rouge">cc</code>) combines it with Miri’s <strong>runtime library</strong> — a separate Rust crate compiled as a static library. The runtime provides all the functions that compiled Miri code calls at execution time: string operations, collection management (creating lists, pushing elements, growing arrays), memory allocation, I/O.</p>

<p>The runtime is type-erased — it doesn’t know about <code class="language-plaintext highlighter-rouge">Int</code> or <code class="language-plaintext highlighter-rouge">String</code>, only about byte sizes. <code class="language-plaintext highlighter-rouge">List&lt;Int&gt;</code> and <code class="language-plaintext highlighter-rouge">List&lt;String&gt;</code> both become the same <code class="language-plaintext highlighter-rouge">MiriList</code> struct with a different <code class="language-plaintext highlighter-rouge">elem_size</code> field. One implementation handles all element types. This is the opposite of Rust’s monomorphization approach (which generates specialized code for each generic instantiation), and the trade-off is worth it: smaller binaries, simpler runtime, at the cost of losing some type-specific optimizations.</p>

<h2 id="the-gpu-first-vision-why-all-of-this-matters">The GPU-first vision: why all of this matters</h2>

<p>Everything I’ve described so far runs on the CPU. But Miri’s architecture was designed with a bigger ambition in mind: <strong>GPU-first computing</strong>.</p>

<p>The MIR already tracks an <code class="language-plaintext highlighter-rouge">ExecutionModel</code> for each function — <code class="language-plaintext highlighter-rouge">Cpu</code>, <code class="language-plaintext highlighter-rouge">Async</code>, <code class="language-plaintext highlighter-rouge">GpuKernel</code>, or <code class="language-plaintext highlighter-rouge">GpuDevice</code>. The type checker already registers GPU-specific built-in types like <code class="language-plaintext highlighter-rouge">Dim3</code> and <code class="language-plaintext highlighter-rouge">GpuContext</code>. The reference counting memory model was specifically chosen because it works without a global garbage collector — a requirement for GPU execution where there’s no runtime thread managing memory.</p>

<p>The backend doesn’t support GPU code generation yet. But the foundation is deliberately designed so that when it does, the programmer’s experience will be seamless: write a function, annotate it with <code class="language-plaintext highlighter-rouge">gpu</code>, and the compiler handles the rest — memory transfers, kernel launches, synchronization. Not two separate languages stitched together with FFI, but one language that targets both CPU and GPU from the same source.</p>

<p>This is the bet: that the next generation of programming languages needs to treat GPU compute as a first-class citizen, not an afterthought bolted on through libraries.</p>

<h2 id="the-trade-offs-that-keep-me-up-at-night">The trade-offs that keep me up at night</h2>

<p>Building a compiler forces you to make dozens of decisions where reasonable people would disagree. Here are the ones I think about most:</p>

<p><strong>Cranelift over LLVM.</strong> LLVM produces better-optimized code, but it’s enormous, slow to compile, and a nightmare to link against. Cranelift trades some code quality for dramatically faster compilation. For a language in active development, where I’m rebuilding the compiler many times a day, that speed matters more. The backend trait abstraction means LLVM can be added later without touching the rest of the pipeline.</p>

<p><strong>Reference counting over garbage collection.</strong> This was the biggest decision. GC would have been easier to implement and handles cycles automatically. But it introduces non-determinism (when does the GC run?), latency spikes, and a unified CPU/GPU memory model becomes much harder — you’d need GC on the CPU side and something else entirely for GPU kernels. Perceus-style automatic RC gives me deterministic destruction and one consistent memory model across both targets, at the cost of slightly more compiler complexity and the future challenge of cycle detection.</p>

<p><strong>Significant whitespace over braces.</strong> This is a taste decision, but it has real consequences. The lexer is more complex because it has to track indentation and emit synthetic tokens. But the resulting code is cleaner, and — importantly — it parses more naturally for LLM-based tooling. When the code’s structure is visible in its indentation rather than hidden in braces, both humans and AI can read it more reliably.</p>

<p><strong>Two-pass type checking over single-pass.</strong> A single pass would be simpler but would require forward declarations or specific ordering. Two passes let you write functions in any order, which is how most programmers think. The cost is an extra walk over the AST, but it’s fast enough that it’s never been a bottleneck.</p>

<p><strong>Type erasure in the runtime over monomorphization.</strong> Monomorphization (generating specialized code for each concrete type) produces faster code. Type erasure (one implementation for all types, parameterized by size) produces smaller binaries and a simpler runtime. For a language still finding its feet, simplicity wins. If profiling later shows the type erasure overhead matters, monomorphization can be added for hot paths.</p>

<p><strong>Multiple error reporting over fail-fast.</strong> The type checker collects all errors and presents them at once. The parser stops at the first error. I’d like to make the parser more resilient eventually, but error recovery in parsing is genuinely hard — you need heuristics for “where does the next valid statement probably start?” — and getting it wrong produces confusing phantom errors. The type checker’s approach is easier because type errors are more localized.</p>

<p>Every one of these decisions could have gone the other way, and the language would still work. That’s the thing about compiler design — there are very few objectively right answers, just trade-offs that align with your priorities. Miri’s priorities are clear: fast iteration, ergonomic code, GPU readiness, and the kind of simplicity that lets a small team (currently just me) keep the whole system in their head.</p>

<p>Years ago, I thought compilers were dark magic. Now I have one that takes a <code class="language-plaintext highlighter-rouge">.mi</code> file and spits out a native binary — and I can trace every step of that journey through code I wrote. The compiler is <a href="https://github.com/miri-lang/miri" class="underline">open source</a>. Next up: making it talk to GPUs :)</p>]]></content><author><name></name></author><category term="miri" /><category term="programming-language" /><category term="compiler" /><category term="software-engineering" /><category term="compiler-design" /><summary type="html"><![CDATA[How Miri’s compiler turns source code into native binaries — from lexing and parsing through type checking, MIR optimization, and code generation with Cranelift.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/miri-compiler-architecture.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/miri-compiler-architecture.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Build a High-Performing Remote Engineering Team</title><link href="https://slavikdev.com/successful-remote-team/" rel="alternate" type="text/html" title="How to Build a High-Performing Remote Engineering Team" /><published>2026-03-14T00:00:00+00:00</published><updated>2026-03-14T00:00:00+00:00</updated><id>https://slavikdev.com/successful-remote-team</id><content type="html" xml:base="https://slavikdev.com/successful-remote-team/"><![CDATA[<p>I first wrote about remote teams in 2019. At the time, remote work was still a perk — something progressive companies offered, while most engineering organizations defaulted to open-plan offices and daily standups around a physical board. Then COVID happened, and suddenly the entire industry was running the largest uncontrolled experiment in distributed work history.</p>

<p>What surprised me wasn’t that remote work <em>worked</em>. I’d been doing it for years and knew it could. What surprised me was how many organizations learned the wrong lessons from it. They either declared remote work a failure and mandated return-to-office, or they kept the Zoom-heavy, always-on model that had burned everyone out during the pandemic and called it “remote culture.”</p>

<p>Neither is right. After working as a CTO in a fully-remote company and later managing distributed engineering teams across Germany and Canada at AutoScout24 and Trader Inc., I’ve landed on a set of principles that actually hold up.</p>

<h2 id="the-real-problem-isnt-productivity">The Real Problem Isn’t Productivity</h2>

<p>Most debates about remote work obsess over the wrong variable: productivity. Managers worry people are slacking off. Employees worry they’re being watched. Both sides produce studies that confirm whatever they already believed.</p>

<p>Here’s what I’ve learned from running distributed platform engineering teams: individual productivity is rarely the problem. Most remote engineers are, if anything, <em>more</em> productive in focused work than their office counterparts. The absence of shoulder-taps, impromptu “quick questions,” and open-plan noise gives them deep work time that offices struggle to provide.</p>

<p>The actual failure modes of remote teams are different and harder to measure:</p>

<ul>
  <li><strong>Information asymmetry.</strong> Some people know things others don’t, and nobody notices because there’s no coffee machine where the gap would surface naturally.</li>
  <li><strong>Decision latency.</strong> Decisions that would take five minutes in a hallway conversation take days of async back-and-forth because the right people aren’t in the same thread.</li>
  <li><strong>Relationship decay.</strong> People who’ve never shared a meal together will default to the most uncharitable interpretation of a Slack message.</li>
  <li><strong>Invisible struggles.</strong> A team member who’s stuck, overwhelmed, or disengaged is much harder to spot when you can’t see their face or energy level in the room.</li>
</ul>

<p>These are the problems worth solving. And none of them are solved by installing surveillance software or mandating cameras-on during meetings.</p>

<h2 id="async-first-is-the-foundation">Async-First Is the Foundation</h2>

<p>The single most impactful shift a remote team can make is going async-first. Not async-only — that’s a different extreme with its own problems. Async-<em>first</em> means the default mode of communication is written and asynchronous, with synchronous meetings reserved for situations that genuinely need them.</p>

<p>Why does this matter so much? Because most remote teams make a critical mistake: they replicate the office experience over video calls. The result is a schedule packed with meetings, with “real work” squeezed into the gaps. Engineers end up in six hours of Zoom calls and then writing code between 7 and 10 PM. That’s not remote work — it’s office work with a shorted commute to the kitchen.</p>

<p>Async-first requires a few things to actually function:</p>

<p><strong>Write things down.</strong> This sounds obvious but almost nobody does it well. Decisions need to be documented where people can find them — not buried in Slack threads that scroll into oblivion within hours. At AutoScout24, we use RFCs and ADRs (Architecture Decision Records) for anything that matters. The discipline of writing a decision down forces clarity that verbal discussions often skip. It also creates an artifact that new team members can find six months later, instead of having to reconstruct context from people’s memories.</p>

<p><strong>Make status visible without asking.</strong> The old “what did you do yesterday?” standup question is a poor man’s visibility system. Better approaches: project boards that reflect actual state, automated deployment notifications, PR dashboards. If a manager needs to ask an engineer what they’re working on, that’s a systems failure, not a people failure.</p>

<p><strong>Default to the permanent channel.</strong> Slack DMs are where information goes to die. Team decisions made in DMs are invisible to everyone who wasn’t in the conversation. Push discussions to public channels, even when it feels slightly awkward. The team member who joins next month will thank you.</p>

<p><strong>Respect time zones ruthlessly.</strong> At AutoScout24, our teams span Germany and Canada — a six-hour time difference. That’s manageable, but only if you design for it. Meetings have to fit the overlap window. Everything else has to work asynchronously. This constraint, paradoxically, makes communication <em>better</em> because it forces you to write clearly enough that someone reading your message hours later can act on it without a follow-up call.</p>

<h2 id="trust-through-systems-not-speeches">Trust Through Systems, Not Speeches</h2>

<p>My 2019 article talked about “trust by default.” It’s a nice sentiment, but it’s incomplete. Trust isn’t a switch you flip. It’s an emergent property of well-designed systems.</p>

<p>What do I mean by that? Consider two scenarios:</p>

<p><strong>Scenario A:</strong> A manager tells their team “I trust you” but has no visibility into what’s happening. When something goes wrong, they panic. Suddenly there are daily check-ins, status reports, and uncomfortable questions. The team feels surveilled. Trust evaporates.</p>

<p><strong>Scenario B:</strong> The team uses a transparent project board. Code is reviewed in public PRs. Deployments are automated and visible. Release metrics are shared. The manager doesn’t need to <em>ask</em> what’s happening because the system makes it visible. They can genuinely trust because they’re not operating blind.</p>

<p>Trust-by-default without transparency is just willful ignorance dressed up as leadership philosophy. Real trust comes from systems that make the right information visible to the right people at the right time — so nobody needs to ask, nobody needs to report, and nobody needs to worry.</p>

<p>This is especially important during the first months with a new team member. Instead of vague expectations and a “figure it out” onboarding, build an onboarding path that makes expectations explicit: here’s how we work, here’s where decisions live, here’s what good looks like, here’s how to get help. The faster someone reaches autonomy, the faster trust develops naturally — not because you decided to “trust by default,” but because they’ve demonstrated competence in a system that makes competence visible.</p>

<h2 id="the-in-person-question">The In-Person Question</h2>

<p>My 2019 take on team building boiled down to: meet your colleagues in person and have a beer. It’s still true, but it’s too simple.</p>

<p>Post-COVID, the question isn’t <em>whether</em> to meet in person but <em>how to make it count</em>. Flying a distributed team to the same city for a week is expensive. If they spend that week in the same meetings they could’ve done on Zoom, you’ve wasted the budget.</p>

<p>In-person time should be optimized for what remote work is bad at:</p>

<ul>
  <li><strong>Relationship building.</strong> Unstructured time matters more than agenda items. Dinners, walks, side conversations — this is where people stop being avatars and start being humans. One real meal together is worth a hundred Slack exchanges for building trust.</li>
  <li><strong>Creative problem-solving.</strong> Whiteboard sessions, design sprints, and messy collaborative work where people interrupt each other productively — these genuinely benefit from physical presence.</li>
  <li><strong>Conflict resolution.</strong> Tensions that fester in async communication often dissolve in person. Tone, body language, and shared laughter do heavy lifting that text can’t.</li>
  <li><strong>Onboarding.</strong> New team members benefit enormously from meeting the team in person early. It creates a foundation of personal connection that makes all subsequent remote interaction smoother.</li>
</ul>

<p>What <em>doesn’t</em> need in-person time: status updates, sprint planning, retrospectives, code reviews, most decision-making. If your offsite agenda is full of these, you’re not using it well.</p>

<p>At AutoScout24, we bring teams together periodically, and the format has evolved through trial and error. The most successful offsites have minimal formal agenda and maximum shared experience. The work that actually needs to happen in person is usually relationship work, not project work.</p>

<h2 id="ai-changes-the-equation">AI Changes the Equation</h2>

<p>Here’s what my 2019 article couldn’t have anticipated: AI coding assistants have fundamentally changed the dynamics of remote engineering work.</p>

<p>When a developer can pair-program with an AI, the cost of being stuck drops dramatically. Questions that used to require interrupting a colleague — <em>”How does this service handle authentication?”</em> or <em>”What’s the pattern for error handling here?”</em> — can often be answered by an AI that has context on the codebase. This is a genuine game-changer for distributed teams, where “just asking someone” carries a much higher latency cost than in an office.</p>

<p>But AI introduces new challenges too. When individual developers can move faster, coordination becomes <em>more</em> important, not less. Two engineers working in parallel with AI assistance can diverge rapidly if they’re not aligned on architecture and approach. The speed that AI enables makes <a href="https://slavikdev.com/crafting-a-vision-for-platform-engineering/" class="underline">shared vision</a> and clear architectural direction more important than ever.</p>

<p>AI also shifts what remote team communication should focus on. Less time explaining <em>how</em> to do things, more time aligning on <em>what</em> and <em>why</em>. The tactical coding questions go to AI. The strategic conversations — what should we build, for whom, and why this approach over that one — still need human discussion. Remote teams that recognize this shift can redesign their communication patterns around it.</p>

<h2 id="what-still-doesnt-work">What Still Doesn’t Work</h2>

<p>Honesty demands acknowledging what remote setups still struggle with, even when done well:</p>

<p><strong>Mentoring junior engineers is harder.</strong> The osmotic learning that happens when a junior developer sits near senior engineers — overhearing design discussions, watching how they debug, absorbing judgment and taste — doesn’t transfer well to remote. You can compensate with structured pairing sessions, recorded design reviews, and deliberate teaching moments, but it requires conscious effort that offices provide for free.</p>

<p><strong>Spontaneous collaboration takes a hit.</strong> The best ideas sometimes come from accidental collisions — two people from different teams chatting at the coffee machine and realizing they’re solving the same problem. Remote work has no equivalent. You can create “virtual serendipity” through cross-team channels, random coffee chats, and internal tech talks, but it’s a pale imitation.</p>

<p><strong>Reading the room is impossible.</strong> In a meeting room, you can see someone’s body language shift when they disagree but don’t want to speak up. On a Zoom call with cameras off — which is most of them, despite policies — you get silence. This makes it harder to surface disagreement, which means decisions get made without genuine buy-in.</p>

<p><strong>Social isolation is real.</strong> Some people genuinely need the social structure of an office. Not everyone thrives working alone from their apartment. And while the “just go to a coworking space” advice helps some, it doesn’t recreate the tribal belonging of a team that shares physical space. Leaders need to watch for signs of isolation and not dismiss them as a remote work skill issue.</p>

<p>None of these are dealbreakers. But pretending they don’t exist — which the “remote work is perfect” crowd tends to do — leads to teams that look functional on paper while people quietly burn out or disengage.</p>

<h2 id="making-it-actually-work">Making It Actually Work</h2>

<p>After years of iterating on distributed team leadership, here’s what I’d tell someone building a remote engineering team today:</p>

<p>Design for async first, then add synchronous rituals where they genuinely help. Not the other way around. Most teams over-index on meetings and under-invest in documentation, which is exactly backward for remote work.</p>

<p>Build transparency into your tools and processes, not into your management habits. If you need to ask someone what they’re doing, you have a systems problem. Fix the system, don’t add more check-ins.</p>

<p>Invest in in-person time, but make it about relationships, not status updates. Two offsites per year with the right format beats monthly all-hands that nobody wants to attend.</p>

<p>Take the mentoring gap seriously. It won’t fix itself. Junior engineers in remote teams need structured support, not just access to a Slack channel.</p>

<p>And stop debating whether remote work “works.” It does. The interesting question — the one worth your leadership energy — is how to make it work <em>well</em>. That’s a design problem, not a policy decision. And like all design problems, it requires iteration, honesty about what’s failing, and the willingness to keep adjusting.</p>

<p>The organizations that thrive with distributed teams aren’t the ones that found a magic formula. They’re the ones that treat remote work as a craft — something to be studied, practiced, and refined — rather than a perk to be granted or revoked based on the latest headline.</p>]]></content><author><name></name></author><category term="leadership" /><category term="remote-team" /><category term="distributed-engineering" /><category term="async-communication" /><category term="engineering-management" /><summary type="html"><![CDATA[Practical strategies for building remote engineering teams that actually work — from async-first communication to trust systems, shaped by years of leading distributed teams.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/running-successful-remote-team.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E525%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/running-successful-remote-team.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E525%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why I’m Building a GPU-First Programming Language in 2026</title><link href="https://slavikdev.com/building-my-own-programming-language-in-2026/" rel="alternate" type="text/html" title="Why I’m Building a GPU-First Programming Language in 2026" /><published>2026-03-01T00:00:00+00:00</published><updated>2026-03-01T00:00:00+00:00</updated><id>https://slavikdev.com/building-my-own-programming-language-in-2026</id><content type="html" xml:base="https://slavikdev.com/building-my-own-programming-language-in-2026/"><![CDATA[<h2 id="the-drift-and-the-draft-circa-2017">The Drift and the Draft (Circa 2017)</h2>

<p>Every slightly unhinged project has an origin story, and Miri’s starts back in 2017. I was between jobs, living mostly off savings, and doing some occasional freelancing. My previous job as a CTO had left me completely burnt out, so I was in absolutely no rush to jump back into the corporate grind.</p>

<p>Instead, I was drifting, floating through a sea of different ideas. At the time, I was a massive fan of Ruby, but its performance left a lot to be desired. I knew Python wasn’t going to win any land speed records either (I actively avoided it back then, though I’d end up using it a lot later).</p>

<p>It got me wondering: <em>Why couldn’t there be a language as ridiculously fast as C++ but as joyfully easy to use as Ruby?</em></p>

<p>So, I did what any procrastinating developer does. I created an empty GitHub repository and started piling up ideas. If you look at the early commits or closed issues from that era, you will laugh. It was a masterclass in productive procrastination—zero actual code, just a mountain of pipe dreams. Later that year, I moved to Germany, and my focus rightfully shifted to my family’s financial stability. The repo sat there gathering digital dust. I had free time, sure, but it was easily swallowed up by family life, sports, hobbies, and my PlayStation.</p>

<h2 id="losingand-findingmyself-20242025">Losing—and Finding—Myself (2024–2025)</h2>

<p>Fast forward to 2024 and 2025. On paper, my career was soaring. I had been promoted to Head of Platform Engineering, managing 40 people across Germany and Canada. But behind the scenes, my personal life was fracturing. I went through a divorce, moved into an empty new home, and felt like I was navigating my entire life on autopilot.</p>

<p>I realized I had completely lost myself.</p>

<p>Looking back at my backlog of side projects, most of them felt soulless—purely business-related and money-driven. There was only one idea that still held any real emotional weight: Miri.</p>

<p>But the world of software had fundamentally shifted since 2017. Large Language Models (LLMs) were suddenly writing code. At first, I felt the bitter sting of a missed opportunity. What was the point of writing a new language now?</p>

<p>I started brainstorming with ChatGPT, fully expecting it to tell me to give up. To my surprise, it didn’t. Instead, it helped me reframe the entire vision: <em>Don’t just build a language for humans. Build a language for a world where AI generates most of the code.</em> Existing languages carry decades of baggage, and AI is forced to use these legacy tools. We need a language designed for agentic engineering, where humans define the intent and AI fills in the safe, verifiable, high-performance implementations. Furthermore, even if AI writes the code, humans still need to read and verify it—so it can’t look like alien hieroglyphics.</p>

<p>Along with the AI revolution came the absolute dominance of GPUs. It hit me that bolting GPU support onto a language as an afterthought is a mistake. Miri needed to be a GPU-first language.</p>

<p>Right now, if you want to write serious GPU code, you’re usually stuck wrestling with C++ and CUDA, which is notoriously unforgiving. There are modern alternatives like Triton and Mojo that build on top of Python, and while they are stepping in the right direction, I believed I could do even better by baking GPU constructs natively into a statically-typed, compiled language.</p>

<p>That realization was the spark. I finally had my “why.” I needed to build Miri for myself—to finally close the loop on a decade-old dream, to demystify how compilers and parallel computing actually work, and to build something unapologetically cool. And with the help of modern AI, I knew I could move exponentially faster than I ever could have in 2017. No more excuses.</p>

<h2 id="why-i-chose-rust-despite-my-initial-grudges">Why I Chose Rust (Despite My Initial Grudges)</h2>

<p>I always assumed I’d write Miri in C++. It’s the battle-tested standard for building compilers, and I already had experience with it. So, I started dusting off my C++ skills—which was an adventure in itself, considering the language had evolved significantly since I last touched it a decade ago.</p>

<p>I had, of course, heard of Rust. But if I’m being completely honest, I just didn’t like how it looked. The syntax felt noisy to me.</p>

<p>But as I was getting back up to speed with C++, the buzz around Rust became impossible to ignore. I kept reading about its legendary memory safety, its insanely robust tooling, and how massive players like Microsoft were actively adopting it for mission-critical systems. The more I looked into it, the more I realized that if I was going to build a modern, high-performance programming language, I needed to build it on a modern foundation. So, I pivoted. Miri is written entirely in Rust.</p>

<p>I jumped into a Udemy course to learn it. Was it easy? Absolutely not. Rust’s learning curve is famously steep. But here’s the beauty of the modern world: learning a notoriously strict language is exponentially faster when you have an LLM in your corner. Instead of banging my head against the wall trying to decode esoteric compiler errors, I could just ask my AI assistant to explain exactly why the borrow checker was yelling at me, and get a crystal-clear answer instantly.</p>

<p>Today, I think Rust is an absolute masterpiece of engineering. Its ecosystem and tooling (like Cargo) are a breath of fresh air. But did I ever grow to love its syntax? Not exactly.</p>

<p>And honestly, that’s the best part. It gave me the perfect excuse to push forward with Miri. I get to borrow Rust’s brilliant architectural concepts and use it to drive Miri’s standard compiler pipeline (moving from Lexer to Parser, AST, MIR, and finally Native Codegen via Cranelift), all while designing a clean, indentation-sensitive syntax that I actually enjoy looking at. It’s the best of both worlds.</p>

<h2 id="how-i-build-a-compiler-with-llms">How I Build a Compiler with LLMs</h2>

<p>My relationship with AI coding assistants has evolved right alongside Miri.</p>

<p>In the beginning, I used ChatGPT purely as a sounding board to refine the project structure and generate initial boilerplate. I was taking compiler development courses, and I wrote the lexer and parser mostly by hand to ensure I actually understood the fundamentals. When I tried to use early agentic coding tools, the results were messy. Because I was still leveling up my Rust skills and learning compiler theory, I spent more time debugging the AI’s hallucinations than writing features.</p>

<p>But as LLMs evolved, they became very useful. Today, my workflow is heavily augmented:</p>
<ul>
  <li>Claude Code &amp; Antigravity (Claude + Gemini): I use these constantly for brainstorming, generating complex MIR (Mid-level Intermediate Representation) lowering passes, writing comprehensive test suites, and refactoring. I honestly cannot imagine building Miri at this pace without them. At the same time, I’m always careful to double-check their work and make sure it aligns with my vision. I have other projects where I can let LLM run wild, without looking over its shoulder, but not with Miri.</li>
  <li>NotebookLM: This is my dedicated research assistant. I use it to synthesize and understand dense, complex topics like memory management, Cranelift backend implementations, and Vulkan/SPIR-V GPU programming.</li>
</ul>

<p>Every now and then, I ask the LLMs to re-validate Miri’s architecture and feasibility. So far, they still agree it needs to exist. And honestly? So do I.</p>]]></content><author><name></name></author><category term="miri" /><category term="programming-language" /><category term="compiler" /><category term="software-engineering" /><summary type="html"><![CDATA[Discover the origin story of Miri, a modern, GPU-first programming language designed for the AI era. Learn why I chose Rust and how LLMs accelerate compiler development.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/building-miri-2026.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/building-miri-2026.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Platform Engineering in 2026: Key Trends &amp;amp; Shifts</title><link href="https://slavikdev.com/platform-engineering-trends-2026/" rel="alternate" type="text/html" title="Platform Engineering in 2026: Key Trends &amp;amp; Shifts" /><published>2025-12-16T00:00:00+00:00</published><updated>2025-12-16T00:00:00+00:00</updated><id>https://slavikdev.com/platform-engineering-trends-2026</id><content type="html" xml:base="https://slavikdev.com/platform-engineering-trends-2026/"><![CDATA[<p>As Head of Platform Engineering at AutoScout24 and Trader Inc., I’ve been working with Platform Engineering practices for years. In that time, I’ve seen what happens when they are present — and what happens when they are not, especially during acquisitions.
In this piece, I focus on the structural shifts I’m seeing, the trade-offs they introduce, and what I would prioritize if I were building or evolving a platform organization today.</p>

<h2 id="why-platform-engineering-is-now-mandatory">Why Platform Engineering Is Now Mandatory</h2>

<p>Platform Engineering has crossed a threshold. It is no longer something only elite tech companies do well. It has become a requirement for any organization operating modern software systems at scale.
The reason is straightforward: system complexity has outpaced human coordination. Microservices, multi-cloud setups, regulatory constraints, and AI workloads don’t compose cleanly through local team decisions alone. Without a platform, organizations compensate with process, tickets, and heroics.</p>

<p>DevOps improved autonomy at the team level, but it never fully solved coordination at the organizational level. “You build it, you run it” only works when teams share well-designed primitives to build and run on. Without those, every team reinvents the same solutions, ownership becomes fragile, onboarding slows down, and operational risk accumulates quietly. In practice, the failure mode looks predictable: every new service requires nuanced CI, custom dashboards, manual IAM setup, and implicit ownership that only exists in someone’s head.</p>

<p>Platform Engineering institutionalizes DevOps. It does so by making the right way the default way, not through mandates, but through paved paths that encode what the organization has already learned.
A useful mental model is simple. Product teams optimize for delivering features. Platform teams optimize for the system that enables delivery. The platform becomes the control plane for software delivery: abstracting infrastructure, encoding standards, and providing self-service that scales across dozens or hundreds of teams. In practical terms, this is where standards, automation, and ownership metadata live.</p>

<p>This distinction becomes especially visible during acquisitions. Teams that grew up without Platform Engineering often rely on tribal knowledge and manual processes. Onboarding them is rarely about tools first. It’s about aligning operating models. What consistently works is introducing the platform as the default delivery path, reinforcing ownership with clear guardrails, and focusing on enablement rather than enforcement.</p>

<p>At scale, this enablement becomes a capability of its own. Platform advocates or enablement teams help business units adopt the platform effectively and prevent local workarounds from eroding consistency over time.
Organizations that delay this investment accumulate organizational debt. Delivery slows despite growing headcount. Incidents increase. Senior engineers become human glue. Cloud and compliance costs rise without clear ownership. By the time leadership reacts, the gap is structural.</p>

<p>Platform Engineering is not about centralization for its own sake. It is a deliberate trade-off: slightly less local freedom in exchange for much higher global throughput and reliability. Teams will still ship software without it — just slower, riskier, and at a higher human cost.</p>

<h2 id="internal-developer-platforms-as-standard-infrastructure">Internal Developer Platforms as Standard Infrastructure</h2>

<p>At a certain scale, an Internal Developer Platform stops being optional. Once you operate dozens of teams, hundreds of services, and multiple cloud environments, you need a single system of record for how software is built, owned, operated, and governed.</p>

<p>High-performing platforms are not just deployment portals. They act as authoritative sources of truth for service ownership, lifecycle, environments, operational metadata, and compliance posture. When this data is reliable, it becomes reusable everywhere else: incident management, KPIs, cost attribution, and operational reviews.</p>

<p>At AutoScout24 &amp; Trader, Backstage plays this role. Service ownership defined in the platform flows into incident reports, Core Tech KPIs, and KTLO attribution. That only works if the data stays clean and enforced. An IDP with stale metadata quickly becomes a liability.
This pattern is not specific to Backstage. Any service catalog can work if ownership is enforced and reused downstream in incidents, reviews, and cost attribution.</p>

<p>Many organizations underestimate the difference between a portal and a platform. Standing up a UI is easy. Keeping the platform relevant over time is hard. Ownership fields decay, catalog entries go stale, paved paths lag reality, and exceptions quietly become the norm. Once trust erodes, developers stop treating the platform as a source of truth, and recovering that trust is expensive.</p>

<p>Early adopters often built heavily customized platforms. We did too, before moving to Backstage. The industry has become more pragmatic since then. Open-source frameworks provide extensibility, managed solutions reduce undifferentiated effort, and internal investment is focused where the platform encodes company-specific rules. The real question is no longer whether something can be built, but whether it helps developers ship better software faster.</p>

<p>A healthy platform is treated like production software. Ownership is enforced at creation time, unused entries are cleaned up, platform data feeds processes developers care about, and using the platform is always easier than bypassing it. When that balance is right, the platform becomes boring, trusted, and essential — exactly what you want.</p>

<h2 id="kubernetes-as-a-foundation-not-a-product">Kubernetes as a Foundation, Not a Product</h2>

<p>Kubernetes itself is no longer the hard part. It is mature and operationally stable in most organizations. What differentiates platforms now is how much of that complexity developers are exposed to.</p>

<p>Kubernetes should not be a developer-facing product. Exposing raw clusters, YAML, and Helm charts to every team increases cognitive load and inconsistency. The emerging pattern is clear: Kubernetes is the compute foundation, the platform is the interface. Developers express intent, the platform translates that intent into resources, policies, and workflows.</p>

<p>At AutoScout24, our paved-path compute is Kubernetes-based. We still support EC2 workloads with standardized AMIs, but Kubernetes is the default and continues to expand. The goal is not forced migration, but making the Kubernetes path clearly superior for most use cases.</p>

<p>GitOps has become the dominant control mechanism in this model. Tools like ArgoCD are foundational for managing desired state, drift detection, and environment promotion. They provide a single source of truth for desired state, automated and auditable deployments, and consistent promotion across environments. Humans own intent. The platform owns reconciliation.</p>

<p>The real leverage comes from layering opinionated workflows on top of Kubernetes: service templates, default security and observability, standardized deployment strategies, and GitOps-driven rollouts. Developers should rarely touch Kubernetes primitives directly. When they do, it should be a conscious exception.</p>

<p>Most organizations run mixed compute models, and that is fine. The mistake is treating them as peers in developer experience. Over time, teams naturally migrate toward the path with the least friction.</p>

<p>What differentiates platforms is not Kubernetes itself, but how invisible it becomes.</p>

<h2 id="ai-as-a-first-class-platform-user">AI as a First-Class Platform User</h2>

<p>AI has rapidly shifted from being just a productivity tool at the edges of development. It is becoming an active participant in the delivery system. That shift forces platforms to treat AI not as a feature, but as a user, with identity, permissions, limits, and accountability.</p>

<p>Early AI adoption was individual and ungoverned. That does not scale. Mature organizations are moving AI capabilities into the platform layer, where they can be governed, audited, integrated into workflows, and bounded by policy and cost controls.</p>

<p>Treating AI as a platform actor means it has explicit identities, scoped roles, quotas, and full observability. AI agents can validate configurations, trigger deployments, or remediate issues — but only within clear boundaries. Unbounded autonomy is operational risk. Platforms exist to contain that risk.</p>

<p>This becomes especially important because AI-generated code often passes syntactic checks while failing operational reality. Platforms increasingly act as safety nets through validation, policy enforcement, and runtime safeguards. Without that layer, AI adoption stalls after the first serious incident.</p>

<p>The next step is multi-agent systems: specialized agents for code generation, security validation, deployment, rollback, and runtime monitoring. Platform Engineering provides the orchestration layer that integrates these agents into CI/CD and GitOps workflows and ensures they act coherently.</p>

<p>A simple rule holds: AI belongs in the platform, not just in the IDE. Centralizing it early creates leverage. Retrofitting guardrails later is far more expensive.</p>

<h2 id="observability-as-a-platform-contract">Observability as a Platform Contract</h2>

<p>Observability has become a crucial part of the platform contract. Every service created through the platform is observable by default.</p>

<p>High-maturity organizations treat observability as a managed platform capability. Logging, metrics, tracing, dashboards, and incident integration are standardized and inherited automatically through paved paths. This reduces variance and enables meaningful cross-service analysis.</p>

<p>OpenTelemetry has effectively become the baseline. Its value is practical: vendor neutrality, consistent semantics, and easier evolution of the observability stack over time.</p>

<p>As systems scale, manual observability does not. Platforms increasingly apply automation and AI to detect anomalies, correlate signals, surface likely root causes, and reduce alert fatigue. The goal is not to replace engineers, but to compress time to insight.</p>

<p>Cost is now inseparable from observability. Telemetry is expensive, and collecting everything is neither useful nor sustainable. Mature platforms enforce sampling, retention policies, telemetry budgets, and observability-as-code. The trade-off is intentional: enough signal to operate safely, not unlimited data.</p>

<h2 id="from-automation-to-self-optimizing-systems">From Automation to Self-Optimizing Systems</h2>

<p>Automation has moved beyond scripts and runbooks. The focus is now on systems that adapt themselves without waiting for human intervention.</p>

<p>Basic self-healing is table stakes. Restarting containers, scaling replicas, and rolling back failed deployments are expected. The real shift is toward predictive and intent-based automation.</p>

<p>Instead of specifying how systems should behave, teams specify what they want: reliability targets, performance objectives, cost boundaries. The platform translates intent into action and continuously adjusts within defined guardrails.</p>

<p>AIOps works only when it is deeply integrated with telemetry, deployment context, and policy. In that setup, platforms can forecast demand, detect anomalies, trigger remediation, and optimize cost and performance in real time.</p>

<p>Autonomy requires boundaries. Clear blast-radius limits, policy-defined ceilings, human approval for structural changes, and full audit trails are non-negotiable. The trade-off is clear: more autonomy yields lower toil and faster response, but only if constraints are enforced rigorously.</p>

<p>Platforms should own routine recovery, optimization, and tuning. Humans should own objectives, policy, and system design.</p>

<h2 id="security-compliance-and-finops-by-default">Security, Compliance, and FinOps by Default</h2>

<p>Security and compliance have shifted from process to platform defaults. If a deployment violates policy, it simply does not happen. Manual reviews cannot keep up with continuous delivery and AI-generated changes.</p>

<p>Policy-as-code has become the enforcement layer. Encryption, IAM, network rules, approved dependencies, compliance constraints, and cost limits are encoded, versioned, tested, and enforced automatically. Developers learn constraints through fast feedback, not audits.</p>

<p>AI changes the threat model further. Platforms must control AI access to data, validate AI-generated artifacts, prevent privilege escalation, and track provenance end to end. Supply chain security, SBOMs, and artifact signing are now baseline expectations.</p>

<p>FinOps follows the same pattern. Cost management has moved into engineering workflows. Platforms surface cost at creation time, enforce budgets automatically, and tie spend to service ownership. This is especially critical for AI workloads, where cost spikes can appear within hours.</p>

<p>The goal is not to turn engineers into accountants. It is to make cost-aware decisions the default path.</p>

<h2 id="platform-as-a-product-teams-as-enablers">Platform as a Product, Teams as Enablers</h2>

<p>Platform Engineering only works when the platform is treated as a product. Developers are the customers. Adoption is earned, not mandated.</p>

<p>This requires product discipline: a roadmap, explicit priorities, non-goals, documentation, onboarding, and continuous feedback. It also requires measurement. Time to first production deployment, deployment frequency, failure rates, developer satisfaction, and toil reduction all matter. Platforms that do not measure outcomes struggle to justify investment or improve intentionally.</p>

<p>As scope grows, ownership must become explicit. Platform Product Managers, Developer Experience Leads, and specialized platform engineers are no longer optional roles. They reflect the reality that prioritization and trade-offs matter.</p>

<p>High-maturity platforms reduce tool sprawl through product decisions. Fewer supported tools, opinionated defaults, and clear exception paths reduce cognitive load and long-term maintenance costs, even if they limit short-term freedom.</p>

<p>Platform teams succeed when they are small, senior, and embedded. They enable product teams rather than replacing them. They collaborate early, observe real usage, and evolve abstractions based on friction, not theory.</p>

<h2 id="closing-thoughts">Closing Thoughts</h2>

<p>Platform Engineering is not about control. It is about leverage.</p>

<p>Organizations that succeed accept a small reduction in local freedom to gain a large increase in global throughput, safety, and sustainability. That trade-off is uncomfortable, but unavoidable.</p>

<p>The platforms that win are boring, trusted, and widely adopted. The ones that fail are optional, fragmented, and under-owned.
If I were starting or course-correcting today, I would focus on four things first:</p>

<ul>
  <li>a reliable service catalog with enforced ownership</li>
  <li>one strong paved path on a primary compute target</li>
  <li>policy and cost gates integrated into delivery</li>
  <li>a small set of outcome metrics tracked consistently</li>
</ul>

<p>The direction of travel is clear. What remains is how deliberately leaders choose to move.</p>]]></content><author><name></name></author><category term="platform-engineering" /><category term="engineering-leadership" /><category term="devex" /><category term="ai" /><category term="cloud" /><category term="security" /><category term="finops" /><category term="leadership" /><summary type="html"><![CDATA[A practical overview of Platform Engineering trends in 2026 for engineering leaders navigating scale, AI, DevEx, security, and cost control.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/platform-engineering-trends-2026.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/platform-engineering-trends-2026.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Balancing Collaboration and Direction in Engineering Leadership</title><link href="https://slavikdev.com/balancing-collaboration-and-direction/" rel="alternate" type="text/html" title="Balancing Collaboration and Direction in Engineering Leadership" /><published>2025-01-04T00:00:00+00:00</published><updated>2025-01-04T00:00:00+00:00</updated><id>https://slavikdev.com/balancing-collaboration-and-direction</id><content type="html" xml:base="https://slavikdev.com/balancing-collaboration-and-direction/"><![CDATA[<p>There’s a particular kind of meeting I’ve come to dread. Everyone’s in the room. The problem is on the table. Ideas are flowing. And an hour later, you walk out with nothing — no decision, no owner, just a vague agreement to “keep discussing.” Everyone feels heard. Nobody feels led.</p>

<p>I’ve run that meeting more times than I’d like to admit.</p>

<p>Early in my leadership career, I believed collaboration was almost always the right answer. If I involved the team in every decision, I’d get better outcomes <em>and</em> higher morale. Two birds, one stone. What I actually got was decision paralysis, frustrated senior engineers, and a team that was polite but quietly losing confidence in my leadership. It took me a while — and some direct feedback from a team lead who told me, bluntly, “We need you to just make the call sometimes” — to understand what I was getting wrong.</p>

<p>The problem wasn’t collaboration itself. The problem was that I’d turned it into a default mode instead of a deliberate tool.</p>

<h2 id="the-collaboration-myth">The Collaboration Myth</h2>

<p>There’s an unspoken orthodoxy in modern engineering leadership: collaboration is inherently good, directive leadership is inherently risky. It shows up in management training, leadership books, and conference talks. “Empower your teams.” “Seek diverse perspectives.” “Create psychological safety.” All good advice — until it becomes dogma.</p>

<p>Here’s what nobody tells you: collaboration has a cost. Every person you include in a decision adds latency. Every brainstorming session you run instead of making a call delays execution. Every “let’s get alignment” meeting is time not spent building. And past a certain point, the returns diminish sharply.</p>

<p>I manage multiple teams across Germany and Canada at AutoScout24 and Trader Inc. At that scale, the cost of unnecessary collaboration is not abstract — it’s measurable in weeks of delayed delivery, in engineers sitting in meetings instead of writing code, in decisions that get relitigated because the last discussion didn’t produce a clear outcome.</p>

<p>The uncomfortable truth is that <em>overcollaboration</em> is at least as common a failure mode as micromanagement. It’s just less visible, because nobody complains about being asked for their opinion. They just quietly disengage when they realize their input doesn’t actually change anything — that the meeting was performative, a ritual of inclusion rather than a genuine decision-making process.</p>

<h2 id="when-collaboration-actively-hurts">When Collaboration Actively Hurts</h2>

<p>Not all decisions benefit from group input. Recognizing which ones do — and which ones don’t — is one of the most important judgment calls an engineering leader makes.</p>

<p><strong>Decisions with clear ownership shouldn’t be collaborative.</strong> If someone owns a system, a domain, or a deliverable, they should make the calls within it. Asking the whole team to weigh in on a database schema that one engineer owns isn’t empowerment — it’s diffusion of responsibility. The owner loses agency, the team loses time, and the decision often gets worse because it becomes a compromise between people with unequal context.</p>

<p><strong>Urgent decisions shouldn’t be collaborative.</strong> During a production incident, a critical escalation, or a hard deadline, the team needs direction, not a brainstorming session. I’ve seen leaders try to run democratic processes during crises — “What does everyone think we should do?” — and the result is always the same: slower response, confused teams, and escalating damage. In those moments, the team is looking for someone to say, “Here’s what we’re doing. Here’s your role. Go.”</p>

<p><strong>Reversible decisions shouldn’t be collaborative.</strong> Jeff Bezos called these “two-way doors” — decisions you can easily undo. If the cost of being wrong is low and the cost of deliberation is high, just decide. Pick the monitoring tool. Choose the sprint cadence. Name the service. Move on. You can always change it later.</p>

<p>Where collaboration genuinely adds value is in decisions that are irreversible, high-stakes, and require expertise you don’t have. Architecture decisions that will shape the system for years. Organizational changes that affect people’s careers. Strategic bets that commit significant resources. These deserve real input from people with relevant context — but even here, “collaboration” doesn’t mean “consensus.”</p>

<h2 id="the-consensus-problem">The Consensus Problem</h2>

<p>Consensus is collaboration’s most dangerous byproduct. It feels democratic and fair, but it optimizes for the wrong thing: agreement rather than quality.</p>

<p>When a group of smart people tries to reach consensus, the result is usually a lowest-common-denominator solution — something everyone can live with but nobody is excited about. The boldest ideas get sanded down. The strongest opinions get diluted. And the person with the most conviction often stays quiet because pushing too hard feels socially risky.</p>

<p>At AutoScout24, we ran into this during a planning session for a major platform initiative. We had strong engineers with legitimate but conflicting perspectives on the architecture. After two hours of discussion, we’d converged on an approach that was… fine. Safe. And, I realized later, worse than any of the individual proposals it was trying to synthesize.</p>

<p>What we do now is different. When we can’t reach agreement — and this happens regularly with a large engineering organization — we use a simple rule: we vote, the majority wins, and we document the minority’s concerns. Not to dismiss them, but to acknowledge the trade-off explicitly. If the minority turns out to be right, we have a record of what they were worried about and can course-correct faster.</p>

<p>This approach does something important: it makes disagreement <em>productive</em> rather than something to be smoothed over. Engineers learn that it’s safe to advocate strongly for their position, because even if they lose the vote, their reasoning is captured and respected. That’s a healthier dynamic than the false harmony of consensus, where disagreement gets buried and resurfaces later as passive resistance or disengagement.</p>

<p>The leader’s job in these moments isn’t to build consensus. It’s to ensure the best arguments get heard, make a decision (or delegate the decision to someone with the right context), and then commit fully — including supporting the outcome publicly, even if it wasn’t your preferred option.</p>

<h2 id="direction-without-micromanagement">Direction Without Micromanagement</h2>

<p>The opposite failure mode — too much direction — is well-documented and easier to spot. Micromanagement kills initiative, erodes trust, and drives your best people to update their LinkedIn profiles. But there’s a subtler version of over-direction that often goes unrecognized: being directive about the <em>how</em> when you should only be directive about the <em>what</em>.</p>

<p>I once worked with a leader who had a strong software engineering background and couldn’t resist making suggestions on database table structure, optimization strategies, and testing approaches. Their intentions were good — they genuinely had relevant expertise. But the effect on the team was corrosive. Engineers stopped proposing their own solutions because they knew the leader would override them anyway. Innovation dried up. The team’s senior engineers, the ones you most want to retain, were the first to lose motivation.</p>

<p>The framework I use now — and the one I enforce with my own engineering managers — comes from how we structure OKRs at AutoScout24. I own the <em>what</em> and the <em>why</em>: the objectives we’re pursuing, the key results that define success, and the reasoning behind them. The teams own the <em>how</em>: the initiatives, the technical approach, the implementation details. Initiative owners identify milestones and are free to pivot whenever they see their approach isn’t moving us toward a Key Result.</p>

<p>This isn’t a loose “figure it out” delegation. It’s structured autonomy. The boundaries are clear. The expectations are measurable. And within those boundaries, teams have genuine freedom — including the freedom to make choices I wouldn’t have made myself.</p>

<p>The hardest part of this, honestly, is shutting up. When a team chooses an approach I think is suboptimal, my instinct is to intervene. But I’ve learned that the cost of occasionally letting a team take a longer path is far lower than the cost of teaching them that their technical judgment doesn’t matter. People learn by doing — including by making mistakes that their boss could have prevented but wisely didn’t.</p>

<p>There are exceptions. If a decision creates significant technical debt that other teams will inherit, or if it violates a security or compliance constraint, I intervene. But I do it by explaining the <em>constraint</em>, not by dictating the <em>solution</em>. “This approach doesn’t align with our platform vision” is direction. “Use Fargate instead of ECS” is micromanagement — even if Fargate is the right answer.</p>

<h2 id="reading-the-room">Reading the Room</h2>

<p>Knowing <em>when</em> to switch between collaborative and directive modes is the real skill. And it’s mostly pattern recognition built over years of getting it wrong.</p>

<p>There are reliable signals for each mode.</p>

<p><strong>Your team needs more direction when:</strong></p>
<ul>
  <li>Discussions keep circling without converging — the same arguments repeat across meetings.</li>
  <li>The team is new, either to the domain or to each other, and hasn’t built the shared context needed for autonomous decision-making.</li>
  <li>Delivery has stalled despite everyone being busy — a classic sign that effort isn’t aligned with priorities.</li>
  <li>The team explicitly asks for it. This happens more often than leadership literature suggests. Good engineers don’t want to be told what to do, but they do want clarity about what matters.</li>
</ul>

<p><strong>Your team needs more collaboration when:</strong></p>
<ul>
  <li>You’re facing a problem outside your own expertise — and you’re honest enough to admit it.</li>
  <li>A decision will significantly affect people who aren’t in the room. Bringing them in isn’t just considerate; it surfaces constraints you’d otherwise miss.</li>
  <li>Trust is low, often after a reorganization or acquisition. Directive leadership in low-trust environments reads as authoritarian. Collaborative leadership builds the relational capital you need before you can be effectively directive.</li>
  <li>The team has deep domain expertise that you don’t. This is increasingly common as organizations scale — no leader can maintain deep technical context across every system their teams own.</li>
</ul>

<p>The transitions between modes matter as much as the modes themselves. When I shift from a collaborative phase to a directive one — say, when a brainstorming discussion needs to become a decision — I make the shift explicit. “We’ve heard the options. Here’s what we’re going with, and here’s why.” No ambiguity. No pretending it’s still collaborative when it isn’t.</p>

<p>Similarly, when I deliberately step back and ask for input, I clarify what kind of input I’m looking for. “I want to hear alternatives” is different from “I want to hear concerns about this plan,” which is different from “I want the team to decide this.” Vague invitations to collaborate produce vague results.</p>

<h2 id="what-scale-changes">What Scale Changes</h2>

<p>Everything I’ve described gets harder as organizations grow. With five engineers, you can operate almost entirely on informal collaboration and gut-feel direction. With forty, you can’t.</p>

<p>At scale, the collaboration-direction balance gets encoded into process whether you design it deliberately or not. Undesigned process is worse — it means the loudest voices drive collaboration, the most senior person’s opinion becomes direction, and the rest of the team oscillates between the two without a clear framework.</p>

<p>What works at AutoScout24 is an explicit structure at multiple levels. Strategic direction — OKRs, platform vision, organizational priorities — flows from leadership. Tactical collaboration — how to solve a specific problem, which approach to take, what trade-offs to accept — lives within teams. And in between, there are mechanisms for cross-team alignment: shared backlogs, regular demo sessions where teams show what they’ve built, and Slack channels where platform decisions get discussed openly before they’re finalized.</p>

<p>The critical insight is that collaboration and direction aren’t opposing forces you balance on a scale. They operate at different levels of abstraction. You can be highly directive about <em>outcomes</em> and highly collaborative about <em>implementation</em> at the same time. In fact, that combination is what the best engineering organizations do — clear alignment on where we’re going, genuine autonomy on how we get there.</p>

<p>When we recently communicated a 54% reduction in high-severity incidents, the outcome was the result of clear direction — this was a priority, these were the metrics, this was the timeline — combined with deep collaboration on the technical solutions. I didn’t tell teams how to reduce incidents. I told them it mattered, gave them the tools and platform support, and got out of the way.</p>

<h2 id="the-leaders-real-job">The Leader’s Real Job</h2>

<p>The balance between collaboration and direction isn’t something you solve once. It shifts with every new team, every new project, every phase of organizational growth. What worked when you managed eight engineers won’t work when you manage forty. What works for a team that’s been together for two years won’t work for a team formed last month from an acquisition.</p>

<p>The job isn’t to find the perfect ratio and lock it in. It’s to develop the judgment to read each situation accurately and the flexibility to adjust — sometimes within the same meeting. To be the person who says, “Let’s discuss this” and also the person who says, “I’ve heard enough, here’s what we’re doing.” And to know which moment calls for which.</p>

<p>That judgment comes from experience, from failure, and from paying attention. It comes from the team lead who told me to stop asking and start deciding. It comes from watching a brilliant engineer disengage because their leader never let them own anything. It comes from seeing a team deliver their best work because their leader gave them a clear objective and then genuinely trusted them to figure it out.</p>

<p>Collaboration and direction aren’t competing values. They’re complementary tools. The skill is knowing which one to pick up — and when to put it down.</p>]]></content><author><name></name></author><category term="leadership" /><category term="collaboration" /><category term="decision-making" /><category term="engineering-management" /><category term="team-leadership" /><summary type="html"><![CDATA[How engineering leaders can balance team collaboration with clear direction — when to seek input, when to decide, and how to avoid the traps of both extremes.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/balancing-collaboration-and-direction.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E400%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/balancing-collaboration-and-direction.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E400%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Craft a Platform Engineering Vision That Works</title><link href="https://slavikdev.com/crafting-a-vision-for-platform-engineering/" rel="alternate" type="text/html" title="How to Craft a Platform Engineering Vision That Works" /><published>2025-01-02T00:00:00+00:00</published><updated>2025-01-02T00:00:00+00:00</updated><id>https://slavikdev.com/crafting-a-vision-for-platform-engineering</id><content type="html" xml:base="https://slavikdev.com/crafting-a-vision-for-platform-engineering/"><![CDATA[<p>Most platform engineering visions fail not because they’re wrong, but because they’re empty. They read like mission statements from a corporate retreat — inspirational enough to hang on a wall, useless for making a single real decision. “We will build a world-class developer platform that empowers teams to deliver value faster.” Great. What does that actually change on Monday morning?</p>

<p>As Head of Platform Engineering at AutoScout24 and Trader Inc., I’ve written platform visions that worked and ones that didn’t. The difference was never eloquence. It was specificity — whether the vision could resolve a real disagreement between two teams, guide a prioritization call, or tell a new hire what we do and don’t build. A vision that can’t do those things is decoration.</p>

<p>This piece covers how to build a platform engineering vision that actually shapes decisions, earns stakeholder trust, and survives contact with organizational reality.</p>

<h2 id="a-vision-is-not-a-strategy">A Vision Is Not a Strategy</h2>

<p>This distinction matters more than it seems.</p>

<p>A vision describes the future state you’re building toward — what the world looks like when your platform succeeds. A strategy describes how you’ll get there. Mixing them up is the most common mistake, and it produces documents that are simultaneously too abstract to guide daily work and too detailed to inspire alignment.</p>

<p>A good platform engineering vision answers one question: <em>What will be true about how this organization builds and operates software in two to three years that isn’t true today?</em></p>

<p>At AutoScout24, our platform vision centers on a specific outcome: any team can go from idea to production without waiting on another team, without reinventing shared infrastructure, and without sacrificing security or compliance in the process. That’s it. It doesn’t mention Kubernetes, Backstage, ArgoCD, or GitHub Actions — those are strategy. The vision is about the developer experience and organizational capability we’re building toward.</p>

<p>This separation is practical, not philosophical. Strategies change. We migrated from Jenkins to GitHub Actions. We adopted Backstage after building a custom service catalog. If the vision had been “standardize on Jenkins,” it would have expired in a year. Because the vision describes the outcome — self-service delivery with guardrails — it survived every strategic pivot underneath it.</p>

<p>When writing your vision, resist the temptation to include implementation details. If it mentions a specific tool, it’s probably strategy. If it describes a capability or organizational property, it’s probably vision.</p>

<h2 id="why-most-platform-visions-fail">Why Most Platform Visions Fail</h2>

<p>I’ve seen platform visions fail in predictable ways. Recognizing these anti-patterns early saves months of misalignment.</p>

<p><strong>The consensus trap.</strong> The vision was workshopped so extensively that every stakeholder’s pet priority got included. The result is a bloated document that tries to be everything — developer productivity, cost reduction, compliance automation, AI enablement, multi-cloud portability — without ranking any of them. When everything is a priority, nothing is. A strong vision makes explicit trade-offs. Ours at AutoScout24 prioritizes developer self-service over maximum flexibility. That means some teams can’t run their preferred exotic stack through the paved path. That’s intentional, and stating it upfront prevents months of renegotiation later.</p>

<p><strong>The abstraction vacuum.</strong> The vision is so high-level it can’t be falsified. “Enable engineering excellence” — what would you have to observe to know you’d failed? If you can’t describe what failure looks like, the vision isn’t specific enough to be useful.</p>

<p><strong>The technology declaration.</strong> The vision is actually a technology roadmap disguised as strategy. “Adopt a service mesh, implement GitOps, migrate to Kubernetes.” These are decisions, not direction. They expire when the technology landscape shifts, and they fail to explain <em>why</em> to anyone outside the platform team.</p>

<p><strong>The borrowed vision.</strong> Lifted from a conference talk or a blog post by a company with a fundamentally different scale, culture, or constraint set. What works for a 5,000-engineer FAANG company doesn’t necessarily apply to a 200-person organization. Your vision must be rooted in your actual pain points, your actual teams, and your actual business context.</p>

<p>The test I use: can a staff engineer on a product team read the vision and immediately understand what it means for their daily work? If the answer is no, it’s not done yet.</p>

<h2 id="building-the-vision-what-actually-works">Building the Vision: What Actually Works</h2>

<p>Crafting a useful platform engineering vision is less about writing and more about listening — then making hard choices about what you heard.</p>

<h3 id="start-with-pain-not-aspiration">Start with pain, not aspiration</h3>

<p>The worst visions start with “Where do we want to be?” The best start with “What’s broken today, and for whom?”</p>

<p>Before writing anything at AutoScout24, I spent weeks in one-on-ones with engineering managers, tech leads, and individual developers across product teams. Not surveys — conversations. Surveys give you aggregated sentiment. Conversations give you stories, and stories reveal the structural problems that surveys miss.</p>

<p>What I heard consistently: teams were spending disproportionate time on infrastructure setup for new services. Each team had slightly different CI configurations, different monitoring setups, different deployment approaches. Onboarding a new engineer to a team meant learning that team’s specific tooling quirks. And when we acquired companies through Trader Inc., the gap widened further — acquired teams had entirely different operating models, and aligning them was a multi-quarter effort driven by tribal knowledge rather than shared infrastructure.</p>

<p>These pain points became the foundation of the vision. Not “build a great platform” but “eliminate the infrastructure tax on product teams and make the right way the default way.”</p>

<h3 id="define-what-you-wont-do">Define what you won’t do</h3>

<p>A vision gains credibility through its boundaries. At AutoScout24, our platform vision explicitly excludes certain things: we don’t build bespoke solutions for individual teams, we don’t optimize for maximum technology choice, and we don’t own application-level concerns like business logic testing. These exclusions matter because they prevent scope creep — the silent killer of platform teams.</p>

<p>Every platform organization faces pressure to absorb adjacent responsibilities. “Can the platform team also own our data pipeline tooling? What about our ML infrastructure?” Without a clear vision that defines boundaries, the answer is always “maybe,” and “maybe” eventually becomes “yes” through accumulated precedent.</p>

<h3 id="make-it-falsifiable">Make it falsifiable</h3>

<p>The best visions have a built-in test for failure. Ours implies measurable conditions: if teams still can’t go from idea to production without filing tickets to another team, we’ve failed. If the paved path is harder to use than the workaround, we’ve failed. If acquired companies take more than two quarters to onboard onto the platform, we’ve failed.</p>

<p>You don’t need to embed OKRs into the vision statement itself, but the vision should point clearly enough at reality that you can construct metrics from it. More on this later.</p>

<h2 id="aligning-teams-without-alignment-theater">Aligning Teams Without Alignment Theater</h2>

<p>Team alignment is one of the most overused phrases in engineering leadership. It usually means “we had a meeting where everyone nodded.” Real alignment — where teams actually change their behavior because they share a common understanding — is harder and rarer.</p>

<h3 id="alignment-is-a-behavior-not-an-event">Alignment is a behavior, not an event</h3>

<p>Running an offsite where everyone agrees on the vision isn’t alignment. Alignment is when a team independently makes a decision that’s consistent with the vision without being told to. That requires the vision to be concrete enough to guide decisions and repeated often enough to be internalized.</p>

<p>At AutoScout24, alignment manifests in daily work through a few mechanisms. Our OKR structure connects platform goals to broader organizational outcomes. Each Key Result maps to something the vision promises — reduced time-to-production, increased paved-path adoption, lower incident rates. Teams own initiatives that contribute to these KRs, and they have autonomy over implementation. The vision provides direction; the teams provide execution.</p>

<h3 id="handle-disagreement-explicitly">Handle disagreement explicitly</h3>

<p>The hard part of alignment isn’t getting people to agree in principle. It’s handling the moment when two reasonable interpretations of the vision conflict.</p>

<p>For example: should the platform team invest in supporting a legacy compute model (EC2-based workloads) or push teams toward Kubernetes migration? Both are defensible. The vision alone doesn’t answer this — but it provides the framework for the conversation. If the vision prioritizes self-service and consistency, then supporting two equally good paths is better than forcing migration and creating resistance. If it prioritizes reducing operational surface area, then a migration timeline with support might be the right call.</p>

<p>We chose to support both — Kubernetes as the default paved path, EC2 with standardized AMIs for teams that aren’t ready to migrate. The vision guided the decision: we optimized for developer autonomy over platform simplicity. We accepted the operational cost of maintaining two paths because forcing migration would have violated the self-service principle the vision is built on.</p>

<p>Document these decisions and their reasoning. They become precedent that teams can reference when similar questions arise later.</p>

<h3 id="cross-team-collaboration-needs-structure-not-enthusiasm">Cross-team collaboration needs structure, not enthusiasm</h3>

<p>“Foster collaboration” is easy to say and hard to operationalize. In practice, cross-team collaboration on platform work requires specific mechanisms:</p>

<p>A shared backlog or roadmap that product teams can see and influence. At AutoScout24, we use a GitHub Project dashboard that’s visible to all engineering teams. They can see what the platform team is working on, what’s coming next, and where their feedback shaped priorities.</p>

<p>Regular demo sessions where platform teams show what they’ve built and product teams share how they’re using it — or not using it. The “not using it” feedback is the most valuable. It reveals where the platform’s mental model diverges from how developers actually work.</p>

<p>Explicit feedback channels. We use Slack and regular demo meetings, but the mechanism matters less than the norm: platform teams actively seek out friction reports and treat them as product feedback, not complaints.</p>

<h2 id="stakeholder-alignment-is-a-translation-problem">Stakeholder Alignment Is a Translation Problem</h2>

<p>Stakeholders don’t resist platform investments because they’re unreasonable. They resist because platform work is often communicated in a language they don’t speak.</p>

<h3 id="speak-outcomes-not-architecture">Speak outcomes, not architecture</h3>

<p>A CTO doesn’t care about your service mesh. They care that deployment failures have dropped by 40% since you introduced standardized rollout strategies. An engineering director doesn’t care about your internal developer portal. They care that new hire onboarding time went from three weeks to five days because every team uses the same development workflow.</p>

<p>When I present platform progress to stakeholders at AutoScout24, I never lead with technology choices. I lead with outcomes: we reduced high-severity incidents by 54%. Time to first production deployment for new services dropped from weeks to days. Paved-path adoption reached a specific percentage across teams. These numbers translate directly into business impact — faster delivery, fewer outages, lower operational cost.</p>

<h3 id="map-the-vision-to-each-stakeholders-priorities">Map the vision to each stakeholder’s priorities</h3>

<p>Different stakeholders hear different things in the same vision. The Head of Security hears “guardrails and compliance by default.” The CTO hears “faster delivery with fewer incidents.” Engineering managers hear “my team spends less time on infrastructure.” Product leaders hear “features ship faster.”</p>

<p>This isn’t manipulation — it’s accurate translation. A good platform vision genuinely serves all these interests. Your job is to make that connection explicit for each audience. I schedule one-on-one conversations with key stakeholders — CTO, Head of Security, engineering directors — before any formal presentation. I learn what they’re measured on, what keeps them up at night, and how the platform connects to their goals. The same vision, communicated through different lenses.</p>

<h3 id="earn-trust-through-early-wins">Earn trust through early wins</h3>

<p>Skeptical stakeholders don’t convert through arguments. They convert through evidence.</p>

<p>During a reliability initiative at AutoScout24, several engineering managers were concerned that platform standardization would slow down feature delivery. Rather than debating the point, we focused on one team as a pilot. We onboarded them onto the paved path, measured the before and after, and shared the results: deployment frequency increased, incidents decreased, and developer satisfaction improved. That single data point was more persuasive than any slide deck.</p>

<p>Identify your most skeptical stakeholder and solve their most visible problem first. The resulting goodwill funds the rest of the roadmap.</p>

<h2 id="measuring-whether-the-vision-is-working">Measuring Whether the Vision Is Working</h2>

<p>A vision without measurement is just an opinion. You need feedback loops that tell you whether the organization is actually moving toward the future state you described.</p>

<h3 id="platform-specific-metrics-that-matter">Platform-specific metrics that matter</h3>

<p>Not all metrics are created equal. The ones that matter most for a platform engineering vision are those that measure whether the platform is fulfilling its promise to developers.</p>

<p><strong>Paved-path adoption rate.</strong> What percentage of teams are using the standardized platform path versus custom solutions? This is the single most important leading indicator. If adoption is low, either the paved path doesn’t solve real problems or it’s harder to use than the alternative. Both are fixable — but only if you’re measuring.</p>

<p><strong>Time to first production deployment.</strong> How long does it take a new service to go from creation to running in production? This measures whether the platform actually removes friction. At AutoScout24, we track this and treat regressions as platform bugs.</p>

<p><strong>Developer satisfaction.</strong> Surveys have limitations, but directional trends matter. We run periodic developer experience surveys that specifically ask about platform tooling. A satisfaction score that’s declining despite feature launches tells you something important.</p>

<p><strong>DORA metrics — with context.</strong> Deployment frequency, lead time, change failure rate, and mean time to recovery are useful but insufficient alone. They measure delivery performance, not platform effectiveness specifically. Track them, but pair them with platform-specific metrics that isolate the platform’s contribution.</p>

<p><strong>Toil reduction.</strong> How much time are developers spending on infrastructure work versus product work? This is hard to measure precisely, but even rough estimates — through time tracking, ticket analysis, or interview data — reveal whether the platform is delivering on its core promise.</p>

<h3 id="iteration-as-a-first-class-practice">Iteration as a first-class practice</h3>

<p>The vision describes a destination. The metrics tell you whether you’re getting closer. The gap between the two is your backlog.</p>

<p>At AutoScout24, we review platform metrics quarterly and adjust priorities based on what the data shows. If paved-path adoption is high but satisfaction is dropping, we know we’re creating compliance without delight — the platform works but it’s painful. If adoption is low but satisfaction among adopters is high, we know we have a distribution problem, not a product problem.</p>

<p>Treat the vision as a living document. Not in the corporate sense of “we’ll update the PowerPoint annually,” but in the operational sense: the vision shapes decisions, decisions produce outcomes, outcomes inform whether the vision needs refinement. At some point, you’ll achieve parts of the vision and need to extend it. That’s a sign of success, not instability.</p>

<h2 id="the-vision-is-the-argument">The Vision Is the Argument</h2>

<p>Here’s what I’ve learned after years of leading platform organizations: the vision is not a document you write and distribute. It’s an argument you make and defend — in roadmap reviews, in prioritization discussions, in architecture decisions, in hiring conversations, and in stakeholder updates.</p>

<p>A strong platform engineering vision does three things. It tells product teams why the platform exists and what it will do for them. It tells stakeholders what outcomes to expect and how to measure them. And it tells the platform team itself what to build, what to skip, and how to make trade-offs when resources are finite.</p>

<p>The platforms that succeed are the ones where the vision is specific enough to be useful, honest enough to acknowledge trade-offs, and embedded deeply enough in daily work that it shapes behavior — not just slides.</p>

<p>Build your vision around the pain your organization actually has. Define it by what you won’t do as much as what you will. Measure it relentlessly. And defend it when the pressure to be everything to everyone inevitably arrives.</p>

<p>The <a href="https://slavikdev.com/platform-engineering-trends-2026/" class="underline">direction of travel for Platform Engineering is clear</a>. What matters now is whether your vision is specific enough to guide the journey.</p>]]></content><author><name></name></author><category term="leadership" /><category term="platform-engineering" /><category term="engineering-vision" /><category term="stakeholder-alignment" /><category term="developer-experience" /><category term="platform" /><summary type="html"><![CDATA[A practical guide to building a Platform Engineering vision that aligns teams and stakeholders — from defining principles to measuring adoption, based on real experience.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/platform-engineering-vision.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/platform-engineering-vision.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E470%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Evolve Legacy Software Systems in 2026</title><link href="https://slavikdev.com/evolve-legacy-software-systems/" rel="alternate" type="text/html" title="How to Evolve Legacy Software Systems in 2026" /><published>2019-10-06T00:00:00+00:00</published><updated>2019-10-06T00:00:00+00:00</updated><id>https://slavikdev.com/evolve-legacy-software-systems</id><content type="html" xml:base="https://slavikdev.com/evolve-legacy-software-systems/"><![CDATA[<p>Your legacy system works. That’s the problem.</p>

<p>It generates revenue, customers depend on it, and nobody wants to touch the payment module that Dave wrote in 2017 before he left. Every feature request takes three times longer than it should. New hires stare at the codebase for weeks before making their first meaningful commit. And somewhere in a Slack channel, an engineer is typing the word “rewrite” for the hundredth time.</p>

<p>I’ve been in this exact situation at multiple companies. At AutoScout24, we inherited systems from acquisitions that were built with completely different philosophies, tech stacks, and quality standards. The temptation to burn it all down and start fresh is always there. But after years of leading platform and engineering teams through these transformations, I can tell you: evolution almost always beats revolution.</p>

<p><em>This article is an updated version of the one I published in 2019</em>. What’s changed since then is <em>how</em> we evolve these systems. AI-assisted development, platform engineering, and modern observability have fundamentally shifted what’s possible. A refactoring effort that would have taken a team months in 2019 can now be measured in weeks — sometimes days.</p>

<h2 id="making-the-invisible-visible">Making the Invisible Visible</h2>

<p>Before you can fix a legacy system, you need to understand how broken it actually is. “It feels slow” and “the code is messy” aren’t arguments that will get you budget. You need data.</p>

<p>The metrics that matter haven’t changed much, but the tools have gotten dramatically better:</p>

<p><strong>Code health:</strong> Static analysis tools like SonarQube or CodeClimate can quantify your technical debt in hours. AI-powered code analysis — through tools like GitHub Copilot, Sourcegraph Cody, or even a well-prompted Claude session — can now identify architectural smells that static analysis misses entirely. Feed your codebase to an LLM and ask it to map coupling between modules. The results might surprise you.</p>

<p><strong>Deployment pipeline:</strong> How long from merge to production? If the answer is “days” or “it depends on who’s deploying,” you have infrastructure debt. DORA metrics (deployment frequency, lead time, change failure rate, mean time to recovery) remain the gold standard here.</p>

<p><strong>Test coverage and runtime:</strong> Coverage below 60% in critical paths means you’re flying blind during refactoring. And if your test suite takes hours, forget about fast iteration — <a href="https://slavikdev.com/do-we-need-unit-tests/" class="underline">tests should enable speed, not slow it down</a>.</p>

<p><strong>Incidents and error rates:</strong> Track severity, frequency, and — critically — which components cause the most incidents. In one of the companies I worked at, this data alone revealed that 80% of our production issues came from 15% of the codebase. That’s where we focused first.</p>

<p><strong>Team velocity trends:</strong> A gradual decline in velocity despite a stable team size is one of the clearest signals of compounding technical debt. Don’t just track it — make it visible to stakeholders who control the budget.</p>

<p>The goal isn’t to create a dashboard for the sake of dashboards. It’s to build a case. When you can show leadership that a specific component causes 40% of your incidents and slows feature delivery by 3x, the conversation shifts from “can we afford to modernize?” to “can we afford not to?”</p>

<h2 id="why-rewrites-still-fail">Why Rewrites Still Fail</h2>

<p>The rewrite pitch is seductive: throw away the mess, start clean, do it right this time. I’ve seen it proposed at every company I’ve worked at. I’ve also seen it fail spectacularly.</p>

<p>A mid-sized company I consulted with decided to rewrite their customer management system — a core product that had grown unwieldy over ten years. The plan was eighteen months. It took three years. During that time, the old system still needed maintenance, splitting the team’s focus and budget. By the time the rewrite shipped, business requirements had evolved so much that the new system already felt outdated.</p>

<p>The fundamental problem with rewrites hasn’t changed: you’re recreating every feature, every edge case, every hard-won piece of domain knowledge — while the world keeps moving. What <em>has</em> changed is that the alternative — incremental evolution — is now significantly more powerful thanks to AI tooling and platform engineering practices.</p>

<p>Here’s the question I always ask teams proposing a rewrite: <em>If the same engineers who built the legacy system are building the replacement, what exactly will be different this time?</em> Unless the answer involves fundamentally better practices and skills, you’re just creating next decade’s legacy system.</p>

<h2 id="the-platform-engineering-angle">The Platform Engineering Angle</h2>

<p>Here’s something that wasn’t part of the conversation in 2019: <a href="https://slavikdev.com/platform-engineering-trends-2026/" class="underline">platform engineering</a> has emerged as one of the most effective ways to evolve legacy systems at scale.</p>

<p>Instead of asking each team to independently modernize their corner of the codebase, you build paved paths — standardized, well-supported ways of doing things that make the modern approach easier than the legacy approach.</p>

<p>At AutoScout24, this means our platform team provides golden paths for compute (Kubernetes-based), observability, CI/CD, and security. When a team needs to modernize a service, they don’t start from scratch. They migrate to the paved path, which comes with built-in best practices, automated compliance checks, and operational support.</p>

<p>The magic of this approach is that it makes evolution the path of least resistance. Teams aren’t “paying down tech debt” — they’re adopting a better developer experience. The framing matters enormously for <a href="https://slavikdev.com/crafting-a-vision-for-platform-engineering/" class="underline">organizational buy-in</a>.</p>

<p>Practically, this looks like:</p>

<p><strong>Internal Developer Platforms (IDPs)</strong> that abstract infrastructure complexity. Tools like Backstage or Port provide a unified interface where teams can provision environments, deploy services, and access documentation — without needing to understand the underlying infrastructure evolution happening beneath them.</p>

<p><strong>Standardized templates and scaffolding.</strong> When spinning up a new service (or migrating an old one), teams start from a template that already includes logging, monitoring, CI/CD configuration, and security baselines. This eliminates an entire category of infrastructure debt.</p>

<p><strong>Automated compliance and governance.</strong> Instead of manual reviews and checklists, policy-as-code tools (OPA, Kyverno) ensure that every deployment meets your standards automatically. Legacy services that can’t pass these checks become visible candidates for modernization.</p>

<p>The platform engineering approach works because it acknowledges a hard truth: you can’t force teams to modernize. But you <em>can</em> make modernization so much easier than the alternative that it becomes the obvious choice.</p>

<h2 id="ai-as-an-accelerator-not-a-silver-bullet">AI as an Accelerator, Not a Silver Bullet</h2>

<p>This is where things have changed the most since 2019 — and even since the early GenAI hype of 2023-2024. AI tooling has matured from “interesting toy” to “genuine force multiplier” for legacy system evolution.</p>

<p><strong>Understanding legacy code.</strong> This is where AI shines brightest. Drop a poorly documented module into Claude, Copilot, or Cursor and ask for an explanation of the business logic, data flow, and hidden assumptions. What used to take a new team member weeks of archaeology now takes hours. I’ve seen engineers use AI to reverse-engineer undocumented APIs, map implicit dependencies, and generate documentation that didn’t exist before.</p>

<p><strong>Refactoring at scale.</strong> AI-assisted refactoring tools can now handle transformations that would have been prohibitively tedious by hand: extracting interfaces, breaking up god classes, converting callback-heavy code to async/await patterns, even migrating between frameworks. The key is treating AI output as a first draft — always review, always test. But that first draft saves enormous time.</p>

<p><strong>Test generation for untested code.</strong> This is arguably the highest-leverage use of AI in legacy modernization. Michael Feathers defined legacy code as code without tests, and he was right — you can’t safely refactor what you can’t test. AI can now generate meaningful test suites for existing code, covering happy paths, edge cases, and error scenarios. These generated tests aren’t perfect, but they provide a safety net that didn’t exist before, making subsequent refactoring dramatically less risky.</p>

<p><strong>Migration assistance.</strong> Moving from one framework, language version, or API to another? AI tools can handle a significant portion of the mechanical translation, freeing engineers to focus on the architectural decisions and business logic that actually require human judgment.</p>

<p>But let’s be honest about the limitations. AI doesn’t understand your business domain deeply. It can’t make architectural trade-off decisions. It hallucinates. It can introduce subtle bugs that look correct at first glance. The teams that get the most value from AI in legacy modernization are the ones that use it as a power tool — wielded by skilled engineers who verify everything — not as an autopilot.</p>

<h2 id="strategies-that-actually-work">Strategies That Actually Work</h2>

<p>With the foundation in place — visibility into the problem, a platform to build on, and AI tools to accelerate the work — here are the strategies I’ve seen succeed repeatedly.</p>

<h3 id="start-with-tests">Start With Tests</h3>

<p>I keep coming back to this because it’s consistently the highest-ROI first move. Before you refactor anything, write tests for the existing behavior. Use AI to generate the initial test suite, then refine it manually. Once you have tests, every subsequent change becomes safer and faster.</p>

<p>The approach is straightforward:</p>
<ul>
  <li>Cover critical user journeys with end-to-end tests first. These are your safety net during any refactoring.</li>
  <li>Add unit tests to modules you plan to change. Apply TDD to all new code.</li>
  <li>Use mutation testing tools to verify your tests actually catch bugs, not just achieve coverage numbers.</li>
</ul>

<h3 id="strangler-fig-pattern">Strangler Fig Pattern</h3>

<p>Wrap legacy components behind clean interfaces. Route traffic gradually from old to new. Kill the old component when traffic hits zero. This pattern works beautifully with platform engineering — the new services live on your paved paths while the legacy components run on their existing infrastructure. Over time, the legacy surface area shrinks until there’s nothing left to strangle.</p>

<h3 id="modular-replacement">Modular Replacement</h3>

<p>Not every component needs gentle evolution. Sometimes a module is so outdated — built on a deprecated framework, running an unsupported language version, or simply incomprehensible — that targeted replacement is the right call. The key is <em>targeted</em>: replace one module at a time, not the entire system.</p>

<p>Identify candidates by looking at: incident frequency, maintenance cost, developer pain (survey your teams — they know which parts of the codebase make them dread Monday mornings), and business criticality of upcoming changes that touch that module.</p>

<h3 id="replatforming">Replatforming</h3>

<p>Moving from on-premises to cloud, from VMs to containers, from manual deployments to GitOps — these infrastructure-level modernizations can extend a legacy system’s life by years without touching the application code. Start with non-critical services, prove the pattern, then migrate systematically.</p>

<p>At AutoScout24, containerizing legacy services and moving them onto our Kubernetes-based platform gave teams access to modern observability, auto-scaling, and deployment tooling — even before they touched a line of application code. The operational improvements alone justified the effort.</p>

<h2 id="building-the-skills-to-sustain-it">Building the Skills to Sustain It</h2>

<p>Tools and strategies are only half the equation. The other half is people.</p>

<p>Why does technical debt accumulate in the first place? Partially because of business pressure, yes. But also because engineers — myself included — don’t always apply what we know. We’ve had decades of accumulated wisdom about clean code, SOLID principles, testing strategies, and system design. The Design Patterns book is over thirty years old. Yet I still see codebases where basic separation of concerns doesn’t exist.</p>

<p>The gap isn’t knowledge — it’s habit. You can name every SOLID principle and still write a 2,000-line controller class because that’s how the existing code looks, and matching the pattern feels easier than fighting it.</p>

<p>Breaking this cycle requires deliberate practice: code reviews that actually discuss design (not just syntax), pair programming on refactoring efforts, internal tech talks where teams share their modernization wins and failures, and — critically — time explicitly allocated for improvement. If every sprint is 100% feature work, technical debt will always grow.</p>

<p>The AI tooling shift makes this even more important, not less. When AI can generate code faster than humans can review it, the ability to evaluate, critique, and improve code becomes the differentiating skill. Engineers who deeply understand design principles will leverage AI to move faster. Engineers who don’t will just produce bad code faster.</p>

<h2 id="the-evolutionary-playbook">The Evolutionary Playbook</h2>

<p>If I were starting a legacy system modernization today, here’s the sequence I’d follow:</p>

<p><strong>Month 1: Measure.</strong> Instrument everything. Get DORA metrics, incident data, code quality scores, and team velocity on a dashboard that leadership can see. Build the case with data.</p>

<p><strong>Months 2-3: Foundation.</strong> Establish your platform — whether that’s a full IDP or just standardized CI/CD pipelines and deployment templates. Define your paved paths. Make the “right way” the easy way.</p>

<p><strong>Months 3-6: Test and strangle.</strong> Use AI to generate test suites for critical components. Begin strangler fig migrations for the highest-pain modules. Celebrate early wins publicly — momentum matters.</p>

<p><strong>Ongoing: Incremental evolution.</strong> Apply the Boy Scout Rule at scale: every change leaves the codebase better than it was found. New features go on paved paths. Legacy components migrate when they need significant changes. The system gets younger over time, not older.</p>

<p>This isn’t a six-month project. It’s a permanent shift in how your organization builds and maintains software. The companies that get this right don’t have a “modernization initiative” — they have an engineering culture where evolution is continuous.</p>

<p>A legacy system is a business asset that’s accumulated technical debt. It’s not a failure — it’s evidence that your software survived long enough to matter. The question isn’t whether to evolve it, but how deliberately you choose to do so.</p>]]></content><author><name></name></author><category term="legacy" /><category term="tech-debt" /><category term="software-engineering" /><category term="platform-engineering" /><category term="ai" /><summary type="html"><![CDATA[Proven strategies for evolving legacy software systems — from AI-assisted refactoring to platform engineering. No risky rewrites required.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/evolve-legacy-software-systems.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467,%20%22author%22=%3E%22Tj%20Holowaychuk%22,%20%22source%22=%3E%22Unsplash%22%7D" /><media:content medium="image" url="https://slavikdev.com/%7B%22url%22=%3E%22posts/evolve-legacy-software-systems.webp%22,%20%22width%22=%3E700,%20%22height%22=%3E467,%20%22author%22=%3E%22Tj%20Holowaychuk%22,%20%22source%22=%3E%22Unsplash%22%7D" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>