<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Code Quality on</title><link>/tags/code-quality/</link><description>Recent content in Code Quality on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 02 Jul 2025 00:00:00 +0000</lastBuildDate><atom:link href="/tags/code-quality/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 10: Over-Engineering CRUD — Not every API needs a hexagonal architecture</title><link>/post/go/go-anti-over-engineering/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-over-engineering/</guid><description>&lt;p&gt;There is a category of service that I have built many times and will build many more times: an API that creates, reads, updates, and deletes records in a relational database, with some validation and authentication. There is nothing glamorous about it, and there is nothing architecturally complex about it either. The correct implementation is straightforward, fast to build, easy to test, and easy to read. The over-engineered implementation has event sourcing, CQRS, a message bus, six layers of abstraction, and takes three months to build something that the straightforward implementation would have shipped in two weeks.&lt;/p&gt;</description></item><item><title>Lesson 8: Linting with golangci-lint — Automate what reviewers shouldn''t waste time on</title><link>/post/go/go-quality-linting/</link><pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-linting/</guid><description>&lt;p&gt;I made a rule for myself a few years ago: if I leave a code review comment about something a tool could have caught, I&amp;rsquo;ve wasted both the author&amp;rsquo;s time and mine. A linter can catch unused variables, missing error checks, shadowed variables, inefficient string concatenation, and dozens of other patterns automatically — in seconds, every commit, without reviewer fatigue. The code review should be about design and correctness, not about whether someone forgot to handle an error returned by &lt;code&gt;rows.Close()&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 9: Fake Clean Architecture — Layers without purpose are just folders</title><link>/post/go/go-anti-fake-clean-arch/</link><pubDate>Tue, 20 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-fake-clean-arch/</guid><description>&lt;p&gt;I cloned a Go repository that was described to me as a &amp;ldquo;clean architecture&amp;rdquo; implementation. It had six layers: &lt;code&gt;handler&lt;/code&gt;, &lt;code&gt;usecase&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;repository&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, and &lt;code&gt;dto&lt;/code&gt;. Every user-related operation required touching at least four files across four packages. When I added a new field to the user profile, I updated the database schema, the &lt;code&gt;model.User&lt;/code&gt;, the &lt;code&gt;dto.UserDTO&lt;/code&gt;, the &lt;code&gt;repository.UserRepository&lt;/code&gt;, the &lt;code&gt;service.UserService&lt;/code&gt;, the &lt;code&gt;usecase.UserUseCase&lt;/code&gt;, and the &lt;code&gt;handler.UserHandler&lt;/code&gt;. Eight files for one field. The indirection was total, the business logic was nowhere — it was scattered across the layers in thin delegating functions that called the layer below and returned the result.&lt;/p&gt;</description></item><item><title>Lesson 10: Using unsafe to Escape the Borrow Checker — The wrong reason</title><link>/post/rust/rust-anti-unsafe-escape-hatch/</link><pubDate>Thu, 24 Apr 2025 09:30:00 +0000</pubDate><guid>/post/rust/rust-anti-unsafe-escape-hatch/</guid><description>&lt;p&gt;I found this in a production codebase:&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-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_or_insert&lt;/span&gt;(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; self, key: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;) -&amp;gt; &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;mut&lt;/span&gt; Value {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;self.map.contains_key(key) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self.map.insert(key.to_string(), Value::default());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// &amp;#34;The borrow checker is being stupid, we know the key exists&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;unsafe&lt;/span&gt; { &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;(self.map.get_mut(key).unwrap() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; Value) }
&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;The comment says it all. The developer hit a borrow checker error — a legitimate one about borrowing &lt;code&gt;self.map&lt;/code&gt; mutably twice — and instead of restructuring the code, they cast through a raw pointer to silence the compiler. This is undefined behavior. The compiler is allowed to assume the mutable references don&amp;rsquo;t alias, and it &lt;em&gt;will&lt;/em&gt; optimize based on that assumption. When it does, your program does something you didn&amp;rsquo;t write.&lt;/p&gt;</description></item><item><title>Lesson 9: Macro Abuse — When a function would do</title><link>/post/rust/rust-anti-macro-abuse/</link><pubDate>Mon, 21 Apr 2025 17:08:00 +0000</pubDate><guid>/post/rust/rust-anti-macro-abuse/</guid><description>&lt;p&gt;I spent an entire afternoon debugging a test failure that turned out to be caused by a macro expanding variable names in a way I didn&amp;rsquo;t expect. The macro was called &lt;code&gt;make_handler!&lt;/code&gt; and it generated HTTP handler functions from a declarative DSL that someone on the team had invented. The &amp;ldquo;DSL&amp;rdquo; saved maybe ten lines of boilerplate per handler. The macro definition was 200 lines of nested &lt;code&gt;macro_rules!&lt;/code&gt; with five recursion levels, three &lt;code&gt;tt&lt;/code&gt; munchers, and hygiene workarounds that I&amp;rsquo;m still not convinced were correct. When a new developer asked how to add a query parameter to a handler, nobody could explain it without first teaching them how the macro worked.&lt;/p&gt;</description></item><item><title>Lesson 8: Premature Optimization — Profile before you optimize</title><link>/post/rust/rust-anti-premature-optimization/</link><pubDate>Sat, 19 Apr 2025 13:42:00 +0000</pubDate><guid>/post/rust/rust-anti-premature-optimization/</guid><description>&lt;p&gt;A teammate once spent three days replacing every &lt;code&gt;String&lt;/code&gt; in our data model with a custom arena-allocated string type. The rationale: &amp;ldquo;String allocations are slow, and we&amp;rsquo;re processing a lot of data.&amp;rdquo; Sounds reasonable, right? After the rewrite, I ran the benchmarks. The improvement was within noise — less than 1%. The actual bottleneck was network I/O to an external API, which accounted for 94% of the request latency. Those three days of intricate unsafe string manipulation? Completely wasted. And the code was now harder to read, harder to maintain, and had a subtle use-after-free bug that we found six weeks later.&lt;/p&gt;</description></item><item><title>Lesson 7: Arc&lt;Mutex&lt;T&gt;&gt; as Default — Reach for channels first</title><link>/post/rust/rust-anti-arc-mutex-default/</link><pubDate>Wed, 16 Apr 2025 20:15:00 +0000</pubDate><guid>/post/rust/rust-anti-arc-mutex-default/</guid><description>&lt;p&gt;There&amp;rsquo;s a specific moment in every Rust developer&amp;rsquo;s journey where they discover &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; and start putting everything in it. I&amp;rsquo;ve been that developer. I had a web service that needed to share a cache between request handlers, and my first instinct was &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;HashMap&amp;lt;String, CachedItem&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;. It worked. Then traffic went up, contention went up, tail latency went up, and I spent a weekend profiling lock contention that shouldn&amp;rsquo;t have existed in the first place — because most of my &amp;ldquo;shared mutable state&amp;rdquo; could have been restructured as message passing.&lt;/p&gt;</description></item><item><title>Lesson 6: Trait Bloat — Interface segregation in Rust</title><link>/post/rust/rust-anti-trait-bloat/</link><pubDate>Tue, 15 Apr 2025 09:55:00 +0000</pubDate><guid>/post/rust/rust-anti-trait-bloat/</guid><description>&lt;p&gt;I worked on a project that had a &lt;code&gt;Storage&lt;/code&gt; trait with twenty-three methods. Twenty-three. It handled reading, writing, deleting, listing, searching, watching for changes, managing permissions, computing checksums, and streaming large files. Every backend — S3, local filesystem, in-memory for tests — had to implement all twenty-three methods. The in-memory test backend had fourteen methods that just returned &lt;code&gt;unimplemented!()&lt;/code&gt;. The local filesystem backend panicked on the permissions methods because POSIX permissions don&amp;rsquo;t map to the trait&amp;rsquo;s model. And every time someone added a new method to the trait, every backend had to be updated — even the ones where the new method made no sense.&lt;/p&gt;</description></item><item><title>Lesson 5: Over-Genericizing — Not everything needs &lt;T&gt;</title><link>/post/rust/rust-anti-over-generic/</link><pubDate>Sun, 13 Apr 2025 11:28:00 +0000</pubDate><guid>/post/rust/rust-anti-over-generic/</guid><description>&lt;p&gt;I reviewed a library last year where the author had made literally everything generic. The HTTP client was generic over the transport, the serializer, the deserializer, the error type, the retry policy, the timeout strategy, and the logger. Using it required spelling out a type signature that looked like this:&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-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; client: &lt;span style="color:#a6e22e"&gt;HttpClient&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TcpTransport&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;TlsConfig&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; JsonSerializer&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;PrettyPrint&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; JsonDeserializer&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;StrictMode&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AppError,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ExponentialBackoff&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;SystemClock&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FixedTimeout,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SlogLogger&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;JsonFormat&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; HttpClient::new(&lt;span style="color:#75715e"&gt;/* ... */&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The kicker? The library was an internal tool used by exactly one team. There was one transport, one serializer, one error type. Every type parameter had exactly one implementation. The author had written an extensible framework for a problem that didn&amp;rsquo;t need extending.&lt;/p&gt;</description></item><item><title>Lesson 4: God Structs — When types do too much</title><link>/post/rust/rust-anti-god-struct/</link><pubDate>Fri, 11 Apr 2025 16:10:00 +0000</pubDate><guid>/post/rust/rust-anti-god-struct/</guid><description>&lt;p&gt;I once opened a file called &lt;code&gt;app.rs&lt;/code&gt; and found a struct with forty-two fields. Forty-two. It held the database connection, the HTTP client, the cache handle, the logger, the config, the metrics collector, the rate limiter, the auth provider, the feature flags, the email sender, the template engine, and about thirty other things I&amp;rsquo;ve blocked from memory. Every function in the codebase took &lt;code&gt;&amp;amp;self&lt;/code&gt; on this monster. Need to send an email? You need the god struct. Parse a config value? God struct. Log a message? Believe it or not, god struct.&lt;/p&gt;</description></item><item><title>Lesson 3: Stringly Typed APIs — Use enums, not strings</title><link>/post/rust/rust-anti-stringly-typed/</link><pubDate>Wed, 09 Apr 2025 08:47:00 +0000</pubDate><guid>/post/rust/rust-anti-stringly-typed/</guid><description>&lt;p&gt;I inherited a Rust codebase once where the entire state machine was driven by string comparisons. The order status could be &lt;code&gt;&amp;quot;pending&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;processing&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;shipped&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;delivered&amp;quot;&lt;/code&gt;, or &lt;code&gt;&amp;quot;cancelled&amp;quot;&lt;/code&gt;. Except sometimes it was &lt;code&gt;&amp;quot;Pending&amp;quot;&lt;/code&gt; with a capital P. And there was one code path that set it to &lt;code&gt;&amp;quot;canceled&amp;quot;&lt;/code&gt; — one L, American spelling. And another that used &lt;code&gt;&amp;quot;CANCELLED&amp;quot;&lt;/code&gt;. The bug lived in production for weeks because nobody could figure out why some orders were getting stuck in a phantom state that didn&amp;rsquo;t match any of the &lt;code&gt;if status == &amp;quot;cancelled&amp;quot;&lt;/code&gt; checks scattered across thirty files.&lt;/p&gt;</description></item><item><title>Lesson 2: unwrap() in Production — Time bombs waiting to explode</title><link>/post/rust/rust-anti-unwrap-abuse/</link><pubDate>Mon, 07 Apr 2025 14:35:00 +0000</pubDate><guid>/post/rust/rust-anti-unwrap-abuse/</guid><description>&lt;p&gt;A service I was responsible for went down at 2 AM on a Saturday because of a single &lt;code&gt;.unwrap()&lt;/code&gt; call on line 847 of a file nobody had touched in months. The function parsed a config value that was supposed to always be present. Somebody changed the config format in a different repo, the value became optional, and that &lt;code&gt;.unwrap()&lt;/code&gt; detonated like a landmine — &lt;code&gt;thread 'main' panicked at 'called Option::unwrap() on a None value'&lt;/code&gt;. Down. Dead. Pager screaming.&lt;/p&gt;</description></item><item><title>Lesson 1: .clone() Everywhere — Hiding ownership problems</title><link>/post/rust/rust-anti-clone-everywhere/</link><pubDate>Sat, 05 Apr 2025 10:22:00 +0000</pubDate><guid>/post/rust/rust-anti-clone-everywhere/</guid><description>&lt;p&gt;I was reviewing a pull request last year from a developer who&amp;rsquo;d been writing Rust for about three months. The code compiled. The tests passed. Everything looked fine — until I ran &lt;code&gt;grep -c '\.clone()' src/&lt;/code&gt; and got back a number that made me physically uncomfortable. Forty-seven clones across six files. The codebase was a data pipeline processing millions of events per hour, and this person had turned every ownership error into a &lt;code&gt;.clone()&lt;/code&gt; call until the compiler stopped yelling.&lt;/p&gt;</description></item><item><title>Lesson 8: Premature Abstraction — Wrong abstraction costs more than duplication</title><link>/post/go/go-anti-premature-abstraction/</link><pubDate>Wed, 02 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-premature-abstraction/</guid><description>&lt;p&gt;There is a principle in software development — &amp;ldquo;Don&amp;rsquo;t Repeat Yourself&amp;rdquo; — that is so widely known it gets abbreviated to DRY and invoked to justify almost any abstraction. The problem is that DRY is a principle about knowledge, not syntax. Two pieces of code that look the same but represent different concepts should stay separate. Two pieces of code that represent the same concept should indeed be unified. Most premature abstractions happen when developers see syntactic similarity and immediately reach for abstraction, before they understand whether the similarity is incidental or fundamental.&lt;/p&gt;</description></item><item><title>Lesson 7: Kill the Utils Package — util.go is where code goes to hide</title><link>/post/go/go-quality-kill-utils/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-kill-utils/</guid><description>&lt;p&gt;Every Go codebase I&amp;rsquo;ve worked on for more than a year has a &lt;code&gt;util&lt;/code&gt; package. Some have a &lt;code&gt;helpers&lt;/code&gt; package. Some have both. I&amp;rsquo;ve seen &lt;code&gt;common&lt;/code&gt;, &lt;code&gt;shared&lt;/code&gt;, &lt;code&gt;misc&lt;/code&gt;, and once, memorably, &lt;code&gt;stuff&lt;/code&gt;. These packages are where code goes when a developer doesn&amp;rsquo;t know where it belongs — which means they&amp;rsquo;re the first place reviewers stop reading carefully, the last place new engineers look when they can&amp;rsquo;t find something, and the primary location of dead code in any codebase over 18 months old.&lt;/p&gt;</description></item><item><title>Lesson 7: Panic as Error Handling — Panic is for bugs, not business logic</title><link>/post/go/go-anti-panic-misuse/</link><pubDate>Tue, 18 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-panic-misuse/</guid><description>&lt;p&gt;I have reviewed Go code from developers who learned other languages where exceptions are the primary error handling mechanism. In Ruby, Python, or Java, you throw an exception and it propagates up the stack until something catches it. In Go, the analogous mechanism — panic — has a very different contract. A panic that is not recovered crashes the entire process. In a web server, that means every in-flight request dies. In a worker process, that means all queued work is dropped.&lt;/p&gt;</description></item><item><title>Lesson 6: Package Cohesion — Everything in a package should belong together</title><link>/post/go/go-quality-package-cohesion/</link><pubDate>Thu, 06 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-package-cohesion/</guid><description>&lt;p&gt;Go packages are the primary unit of code organization. They determine what&amp;rsquo;s visible to whom, what gets compiled together, and — most importantly — they communicate intent to every engineer who reads the codebase. A package with high cohesion says something true and useful: &amp;ldquo;everything here is about orders&amp;rdquo; or &amp;ldquo;everything here handles HTTP middleware.&amp;rdquo; A package with low cohesion says nothing — it&amp;rsquo;s a filing cabinet where things went when nobody knew where else to put them.&lt;/p&gt;</description></item><item><title>Lesson 6: Swallowing Errors — The silent failure that cost us 3 hours</title><link>/post/go/go-anti-swallowing-errors/</link><pubDate>Sun, 12 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-swallowing-errors/</guid><description>&lt;p&gt;We had a deployment where user preferences stopped being saved. New preferences were accepted by the API — the endpoint returned 200 — but nothing was written to the database. After three hours of debugging we found it: a &lt;code&gt;defer rows.Close()&lt;/code&gt; inside a transaction helper that was discarding the error from &lt;code&gt;tx.Commit()&lt;/code&gt;. The commit was failing silently, the defer returned without error, and the handler sent a success response. The only indication anything was wrong was a metrics counter nobody had set up an alert for.&lt;/p&gt;</description></item><item><title>Lesson 5: Small Functions Win — If you can''t name it clearly, it does too much</title><link>/post/go/go-quality-small-functions/</link><pubDate>Fri, 06 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-small-functions/</guid><description>&lt;p&gt;There is a simple test I apply to any function I&amp;rsquo;m about to merge: can I name it clearly without using &amp;ldquo;and&amp;rdquo;? If the most honest name for a function is &lt;code&gt;validateAndSaveAndNotifyUser&lt;/code&gt;, the function is three functions pretending to be one. The naming test doesn&amp;rsquo;t lie — it&amp;rsquo;s a direct readout of the function&amp;rsquo;s responsibility. I&amp;rsquo;ve seen this test convince engineers who were unmoved by SOLID principles or Uncle Bob quotes, because it&amp;rsquo;s visceral: if you can&amp;rsquo;t say what the function does in a few words, you already know something is wrong.&lt;/p&gt;</description></item><item><title>Lesson 5: Ignoring Context — The cancellation nobody checked</title><link>/post/go/go-anti-ignoring-context/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-ignoring-context/</guid><description>&lt;p&gt;I spent a Tuesday afternoon tracking down why our service continued doing expensive database work after clients had long since disconnected. An HTTP client with a 3-second timeout would cancel the request, but our handler would still run three downstream database queries that together took 8 seconds. The handler was checking for errors but never checking the context. By the time it finished, the client had retried twice, and we were now running three copies of the same 8-second work simultaneously.&lt;/p&gt;</description></item><item><title>Lesson 4: Code Review Heuristics — What to look for in a Go PR</title><link>/post/go/go-quality-code-review/</link><pubDate>Sun, 20 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-code-review/</guid><description>&lt;p&gt;A good code review is not a diff-reading exercise. It&amp;rsquo;s a transfer of understanding — the reviewer asks &amp;ldquo;do I understand what this code does, why it does it, and what it doesn&amp;rsquo;t do?&amp;rdquo; If the answer to any of those is no, that&amp;rsquo;s a comment, not a nitpick. I&amp;rsquo;ve done hundreds of Go reviews and the feedback I give clusters into the same ten or fifteen patterns so reliably that I eventually wrote them down as a checklist. This lesson is that checklist, with examples.&lt;/p&gt;</description></item><item><title>Lesson 4: Channel Misuse — You used a channel where a mutex would do</title><link>/post/go/go-anti-channel-misuse/</link><pubDate>Tue, 15 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-channel-misuse/</guid><description>&lt;p&gt;Go&amp;rsquo;s channels are genuinely great. They make certain concurrent programming patterns — pipelines, fan-out, fan-in, worker pools — natural and readable. Because they are idiomatic and distinctly Go-like, there is a tendency among Go developers to reach for them first whenever concurrency is involved. The result is code that uses channels to protect shared state, which is what mutexes are for, or code that passes one value through a channel with ceremony that could be replaced by a function call.&lt;/p&gt;</description></item><item><title>Lesson 3: Global Mutable State — The variable that breaks every test</title><link>/post/go/go-anti-global-state/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-global-state/</guid><description>&lt;p&gt;There was a test suite I worked on where tests passed individually but failed when run together. The failure pattern was non-deterministic — sometimes test A broke test B, sometimes test C broke test A, and it changed with the &lt;code&gt;-count&lt;/code&gt; flag. After two hours of bisecting, we found it: a package-level &lt;code&gt;var config Config&lt;/code&gt; that every test modified by calling &lt;code&gt;loadConfig(&amp;quot;testdata/some-fixture.json&amp;quot;)&lt;/code&gt;. The tests were sharing state without knowing it, and the one that ran last set the config for all the others.&lt;/p&gt;</description></item><item><title>Lesson 3: Idiomatic Naming — Names are your documentation</title><link>/post/go/go-quality-naming/</link><pubDate>Wed, 04 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-naming/</guid><description>&lt;p&gt;When I review Go code, naming problems tell me more about the author&amp;rsquo;s understanding of the codebase than almost anything else. A function called &lt;code&gt;HandleRequest&lt;/code&gt; that parses JSON, queries a database, formats a response, and writes to a log tells me its author didn&amp;rsquo;t know what it does either. A variable called &lt;code&gt;data&lt;/code&gt; in a function that handles three different kinds of data tells me the author stopped thinking halfway through. Names are the first layer of documentation — they&amp;rsquo;re read far more often than comments, and unlike comments, they can&amp;rsquo;t drift out of sync with the code.&lt;/p&gt;</description></item><item><title>Lesson 2: Giant God Structs — If your struct has 30 fields, it has 30 problems</title><link>/post/go/go-anti-god-structs/</link><pubDate>Thu, 25 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-god-structs/</guid><description>&lt;p&gt;I inherited a Go service where the primary domain object was a struct with 47 fields. Some were database columns, some were computed from other fields, some were HTTP response projections, some were used only during a specific workflow stage, and a handful were there because someone once needed a flag and adding a field to the God struct was the path of least resistance. The struct was everywhere — passed between functions, serialized to JSON, written to the database, and used as a GraphQL response type. Every change to it rippled through the entire codebase.&lt;/p&gt;</description></item><item><title>Lesson 2: Spotting Over-Abstraction — When your abstraction is the problem</title><link>/post/go/go-quality-over-abstraction/</link><pubDate>Wed, 24 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-over-abstraction/</guid><description>&lt;p&gt;There&amp;rsquo;s a version of Go code I see in almost every codebase that&amp;rsquo;s been touched by developers who came from Java or C# — it&amp;rsquo;s covered in interfaces, repositories, factories, and service layers that all wrap exactly one concrete implementation. No tests mock these interfaces. No second implementation exists. The abstraction is doing nothing except adding indirection. I&amp;rsquo;ve written code like this myself, and the tell is always the same: when something breaks, I have to jump through five files to understand what happens when I call &lt;code&gt;CreateUser&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 1: Interface Everywhere Syndrome — Not every dependency needs an interface</title><link>/post/go/go-anti-interface-everywhere/</link><pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-interface-everywhere/</guid><description>&lt;p&gt;When I came to Go from a Java background, I brought some habits with me that made my code worse, not better. The most persistent one was defining an interface for every dependency, regardless of whether there was any realistic chance of substituting an alternative implementation. I wrote &lt;code&gt;UserRepositoryInterface&lt;/code&gt;, &lt;code&gt;EmailSenderInterface&lt;/code&gt;, &lt;code&gt;LoggerInterface&lt;/code&gt; — an entire shadow type system that mirrored every concrete type in the codebase. Every file had a corresponding interface file. The codebase was twice as large as it needed to be and no easier to test.&lt;/p&gt;</description></item><item><title>Lesson 1: Refactoring Go Code Safely — Change structure without changing behavior</title><link>/post/go/go-quality-refactoring/</link><pubDate>Sun, 16 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-refactoring/</guid><description>&lt;p&gt;Refactoring is the one activity that makes codebases better without adding features — and it&amp;rsquo;s also the activity most likely to introduce bugs if done carelessly. I learned this the hard way on a payments service where I renamed a function, ran the tests, saw green, deployed, and watched a webhook handler silently stop processing because it had been calling the old function name through a string-based registry I hadn&amp;rsquo;t touched. The tests were green because the old function still existed — I just hadn&amp;rsquo;t deleted it yet. The refactor was correct; the process was not.&lt;/p&gt;</description></item></channel></rss>