<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Engineering-Practices on rjrowland</title><link>https://rjrowland.com/series/engineering-practices/</link><description>Recent content in Engineering-Practices on rjrowland</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 07 Aug 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://rjrowland.com/series/engineering-practices/index.xml" rel="self" type="application/rss+xml"/><item><title>The 6 Principles: How I Try to Work as an Engineer</title><link>https://rjrowland.com/posts/engineering-practices-the-6-principles/</link><pubDate>Fri, 07 Aug 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-the-6-principles/</guid><description>&lt;p&gt;I don&amp;rsquo;t meet this bar every day. I wrote it down because writing it down is how I hold myself to it. This series lays out the practices I try to work by as an engineer: patterns and anti-patterns across data modeling, delivery, code quality, code structure, architecture, and performance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="1-make-impossible-states-unrepresentable--and-parse-dont-validate"&gt;1. Make impossible states unrepresentable — and parse, don&amp;rsquo;t validate&lt;/h2&gt;
&lt;p&gt;This is the single highest-leverage type-level habit. Every time you reach for &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, or &lt;code&gt;string&lt;/code&gt; to represent a domain concept, ask: can this type contain values that are invalid in the domain?&lt;/p&gt;</description></item><item><title>The Highest-Leverage Work Is Never Feature Work</title><link>https://rjrowland.com/posts/engineering-practices-career-leverage/</link><pubDate>Thu, 06 Aug 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-career-leverage/</guid><description>&lt;p&gt;Senior engineers amplify through team success, not individual output. The leverage formula applies directly to career development: the highest-return activities multiply team effectiveness, they don&amp;rsquo;t just add to your own.&lt;/p&gt;
&lt;p&gt;The key insight: the most visible, most impactful technical contributions at a company level are almost never feature work. They&amp;rsquo;re the infrastructure improvements that make feature work easier, safer, or faster for everyone else.&lt;/p&gt;
&lt;h2 id="finding-the-opportunity"&gt;Finding the opportunity&lt;/h2&gt;
&lt;p&gt;Diagnose before you propose. Ask:&lt;/p&gt;</description></item><item><title>Stuck Designing or Stuck Building?</title><link>https://rjrowland.com/posts/engineering-practices-bottom-up-top-down/</link><pubDate>Tue, 04 Aug 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-bottom-up-top-down/</guid><description>&lt;p&gt;This tension runs underneath most of the practices in this series. Making it explicit helps you pick the right mode at each stage of the work.&lt;/p&gt;
&lt;p&gt;Top-down starts from the highest-level model, requirements, domain, architecture, and works toward implementation. It asks: what are we building? What does success look like? What structure should this system have?&lt;/p&gt;
&lt;p&gt;Bottom-up starts from concrete, working pieces and builds upward. It asks: what actually works? What do the real constraints look like? What can I demonstrate right now?&lt;/p&gt;</description></item><item><title>Code Review Is Wasted on Style Comments</title><link>https://rjrowland.com/posts/engineering-practices-code-review/</link><pubDate>Fri, 31 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-code-review/</guid><description>&lt;p&gt;Code review has its highest leverage at the design level, not the style level. The questions that matter most are structural.&lt;/p&gt;
&lt;h2 id="are-invariants-maintained"&gt;Are invariants maintained?&lt;/h2&gt;
&lt;p&gt;What does the system depend on being true, and does this change preserve that? This question catches whole classes of bugs a line-by-line read misses entirely.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// The invariant: a settled transfer always has a SettledAt timestamp.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// This PR adds a new status without touching that guarantee.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;markSettled&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;context&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Context&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;transferID&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TransferID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;network&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;error&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Transfers&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Update&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;transferID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;TransferUpdate&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;Status&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;settled&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;Network&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;network&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// SettledAt never gets set — nothing enforces it, nothing warns you&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Line by line, this reads fine: it updates a status, it takes a network, it compiles. The bug only shows up when you ask the invariant question directly — &amp;ldquo;settled transfers always have a &lt;code&gt;SettledAt&lt;/code&gt;&amp;rdquo; was true before this PR and isn&amp;rsquo;t after it. A line-by-line read has no way to know that invariant exists unless someone states it and checks the diff against it.&lt;/p&gt;</description></item><item><title>How to Learn a Codebase With No Docs</title><link>https://rjrowland.com/posts/engineering-practices-learning-a-codebase/</link><pubDate>Wed, 29 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-learning-a-codebase/</guid><description>&lt;p&gt;The hardest case: no tests, no architecture decision records, no diagrams. The tactics shift from consuming documentation to producing it as a byproduct of exploring.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what that actually looks like, walked through on one example: you&amp;rsquo;ve just joined a team maintaining &lt;strong&gt;Ledger&lt;/strong&gt;, an internal core-banking service written in Go. Nobody&amp;rsquo;s written a README beyond &lt;code&gt;make dev&lt;/code&gt;. You have a week before you&amp;rsquo;re expected to ship anything.&lt;/p&gt;
&lt;h2 id="start-outside-the-code"&gt;Start outside the code&lt;/h2&gt;
&lt;p&gt;Use the product as a user before you read a line of source. Check the error monitoring dashboards, Sentry, Datadog, early: they show where the system actually breaks, which tells you more than reading happy-path code ever will. The production error log is an honest account of what the system struggles with. The codebase only shows you what the engineers thought would happen.&lt;/p&gt;</description></item><item><title>If Running Tests Takes Willpower, They're Too Slow</title><link>https://rjrowland.com/posts/engineering-practices-fast-feedback-loops/</link><pubDate>Tue, 28 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-fast-feedback-loops/</guid><description>&lt;h2 id="the-layers-of-feedback"&gt;The layers of feedback&lt;/h2&gt;
&lt;p&gt;The practices in this series keep circling back to feedback, for a reason: the speed of your feedback loop sets the speed of your learning, and the speed of your learning sets almost everything else about how effective you are.&lt;/p&gt;
&lt;p&gt;Feedback loops come in layers, and the right fix is different at each one.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Editor feedback.&lt;/strong&gt; Compiler errors, &lt;code&gt;go vet&lt;/code&gt;, &lt;code&gt;staticcheck&lt;/code&gt;, inline warnings. Zero latency, the tightest loop that exists. Investing in it, stricter linting, more complete type coverage, pays back with compounding interest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unit tests in watch mode.&lt;/strong&gt; Sub-second, on the file you just changed. If your unit tests don&amp;rsquo;t run automatically on save (&lt;code&gt;gotestsum --watch&lt;/code&gt;, or your editor&amp;rsquo;s built-in runner), you&amp;rsquo;re leaving real developer experience on the table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local rebuild-and-run.&lt;/strong&gt; A change reflected without a full redeploy. Trivial to script with &lt;code&gt;air&lt;/code&gt; or &lt;code&gt;reflex&lt;/code&gt;, and often just left off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration tests.&lt;/strong&gt; Should run in seconds, not minutes. A slow test doesn&amp;rsquo;t get run. That&amp;rsquo;s not a discipline problem, it&amp;rsquo;s an incentives problem: a four-minute suite gives engineers a reason to skip it before pushing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI.&lt;/strong&gt; Parallelize aggressively. A twenty-minute pipeline is a morale tax paid on every single PR, and the cost isn&amp;rsquo;t just the twenty minutes, it&amp;rsquo;s the context switch of sitting there waiting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The heuristic: if running tests takes willpower, the tests are too slow. Running them should be the path of least resistance, not a deliberate act you have to talk yourself into.&lt;/p&gt;</description></item><item><title>Profile First, Optimize Second</title><link>https://rjrowland.com/posts/engineering-practices-performance/</link><pubDate>Mon, 27 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-performance/</guid><description>&lt;h2 id="measure-first-know-your-bottlenecks"&gt;Measure first, know your bottlenecks&lt;/h2&gt;
&lt;p&gt;Performance intuition is wrong more often than it&amp;rsquo;s right. The thing you&amp;rsquo;re certain is the bottleneck almost never turns out to be. Profile first and let the data decide where effort goes. There&amp;rsquo;s no reliable shortcut around this.&lt;/p&gt;
&lt;p&gt;Most web application code spends its time waiting, not computing. A request handler calls the database, waits, calls another service, waits, maybe hits a cache, waits again — and the CPU sits idle through nearly all of it. That&amp;rsquo;s why the common bottleneck categories skew so heavily toward I/O, roughly in this order of frequency:&lt;/p&gt;</description></item><item><title>Design for the System You Have, Not the One You Imagine</title><link>https://rjrowland.com/posts/engineering-practices-architecture/</link><pubDate>Sat, 25 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-architecture/</guid><description>&lt;h2 id="strangler-fig-not-the-big-bang-rewrite"&gt;Strangler fig, not the big bang rewrite&lt;/h2&gt;
&lt;p&gt;The big bang rewrite has a seductive pitch: the existing system is a mess, the new one will be clean, we&amp;rsquo;ll migrate users when it&amp;rsquo;s ready. What actually happens: the rewrite starts with &amp;ldquo;just the core functionality&amp;rdquo; and spends years discovering that the old system&amp;rsquo;s quirks were load-bearing. Edge cases nobody documented, accumulated over years. Integrations with external systems nobody wrote down. Data migration complexity nobody scoped. Behavioral requirements nobody stated because everyone assumed they were obvious.&lt;/p&gt;</description></item><item><title>Don't Touch Code You Don't Understand Yet</title><link>https://rjrowland.com/posts/engineering-practices-code-quality/</link><pubDate>Fri, 24 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-code-quality/</guid><description>&lt;h2 id="characterization-tests-one-dimension-at-a-time"&gt;Characterization tests, one dimension at a time&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;The term &amp;ldquo;characterization tests&amp;rdquo; comes from Michael Feathers&amp;rsquo; &lt;a href="https://www.oreilly.com/library/view/working-effectively-with/0131177052/" target="_blank"&gt;Working Effectively with Legacy Code&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Refactoring without tests isn&amp;rsquo;t refactoring. It&amp;rsquo;s rewriting with optimism. &amp;ldquo;Looks equivalent&amp;rdquo; is not &amp;ldquo;is equivalent.&amp;rdquo; The subtle behavioral change hiding in an untested path finds its way to production six months later, &lt;code&gt;git blame&lt;/code&gt; points at a commit that moved some files around, and nobody can tell what the intended behavior even was.&lt;/p&gt;</description></item><item><title>Ship Small, Ship Boring, Know When You're Done</title><link>https://rjrowland.com/posts/engineering-practices-delivery-feedback/</link><pubDate>Thu, 23 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-delivery-feedback/</guid><description>&lt;h2 id="vertical-slices-not-horizontal-layers"&gt;Vertical slices, not horizontal layers&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a failure mode I&amp;rsquo;ve watched play out on almost every large feature: the team builds all of one layer before starting the next. Every database table first, then every endpoint, then every screen. Progress looks real — the database layer is &amp;ldquo;done,&amp;rdquo; the API is 70% there — right up until week six, when integration starts. The data model was built on assumptions that turned out wrong. The API contract doesn&amp;rsquo;t match what the UI actually needs. Two weeks of rework follows.&lt;/p&gt;</description></item><item><title>Get the Data Model Right, Get Everything Else for Free</title><link>https://rjrowland.com/posts/engineering-practices-data-domain/</link><pubDate>Wed, 22 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-data-domain/</guid><description>&lt;p&gt;The data model is the most consequential decision in a system, and teams make it too casually. Code gets rewritten constantly. Schemas outlast teams. A wrong column name costs you a migration. A missing foreign key costs you a redesign. A misunderstood entity relationship can invalidate months of work. Get the model right before any code exists, because that&amp;rsquo;s the cheapest point at which you&amp;rsquo;ll ever get to fix it.&lt;/p&gt;</description></item><item><title>Push Complexity to the Edges</title><link>https://rjrowland.com/posts/engineering-practices-push-complexity-to-edges/</link><pubDate>Tue, 21 Jul 2026 09:00:00 -0500</pubDate><guid>https://rjrowland.com/posts/engineering-practices-push-complexity-to-edges/</guid><description>&lt;h2 id="push-ifs-up-and-fors-down"&gt;Push ifs up and fors down&lt;/h2&gt;
&lt;p&gt;Alex Kladov named this precisely in &lt;a href="https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-down.html" target="_blank"&gt;&amp;ldquo;Push Ifs Up and Fors Down&amp;rdquo;&lt;/a&gt;: two structural habits most engineers apply by instinct sometimes, without ever generalizing them.&lt;/p&gt;
&lt;p&gt;Push ifs up: conditional logic belongs as high in the call stack as it can go. Instead of a function checking a condition internally and doing nothing when it doesn&amp;rsquo;t apply, let the caller decide whether to call it. The function becomes unconditional, simpler, more composable. Its signature stops lying: it does X, full stop, instead of &amp;ldquo;maybe does X depending on internal state.&amp;rdquo;&lt;/p&gt;</description></item></channel></rss>