<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Go Anti-Patterns &amp; Code Smells on Atharva Pandey</title><link>https://atharva.page/series/go-anti-patterns--code-smells/</link><description>Recent content in Go Anti-Patterns &amp; Code Smells on Atharva Pandey</description><generator>Hugo</generator><language>en-us</language><copyright>Copyright ©</copyright><lastBuildDate>Wed, 02 Jul 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://atharva.page/series/go-anti-patterns--code-smells/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 10: Over-Engineering CRUD — Not every API needs a hexagonal architecture</title><link>https://atharva.page/post/go/go-anti-over-engineering/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>https://atharva.page/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 9: Fake Clean Architecture — Layers without purpose are just folders</title><link>https://atharva.page/post/go/go-anti-fake-clean-arch/</link><pubDate>Tue, 20 May 2025 00:00:00 +0000</pubDate><guid>https://atharva.page/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 8: Premature Abstraction — Wrong abstraction costs more than duplication</title><link>https://atharva.page/post/go/go-anti-premature-abstraction/</link><pubDate>Wed, 02 Apr 2025 00:00:00 +0000</pubDate><guid>https://atharva.page/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: Panic as Error Handling — Panic is for bugs, not business logic</title><link>https://atharva.page/post/go/go-anti-panic-misuse/</link><pubDate>Tue, 18 Feb 2025 00:00:00 +0000</pubDate><guid>https://atharva.page/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: Swallowing Errors — The silent failure that cost us 3 hours</title><link>https://atharva.page/post/go/go-anti-swallowing-errors/</link><pubDate>Sun, 12 Jan 2025 00:00:00 +0000</pubDate><guid>https://atharva.page/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: Ignoring Context — The cancellation nobody checked</title><link>https://atharva.page/post/go/go-anti-ignoring-context/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><guid>https://atharva.page/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: Channel Misuse — You used a channel where a mutex would do</title><link>https://atharva.page/post/go/go-anti-channel-misuse/</link><pubDate>Tue, 15 Oct 2024 00:00:00 +0000</pubDate><guid>https://atharva.page/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>https://atharva.page/post/go/go-anti-global-state/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><guid>https://atharva.page/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 2: Giant God Structs — If your struct has 30 fields, it has 30 problems</title><link>https://atharva.page/post/go/go-anti-god-structs/</link><pubDate>Thu, 25 Jul 2024 00:00:00 +0000</pubDate><guid>https://atharva.page/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 1: Interface Everywhere Syndrome — Not every dependency needs an interface</title><link>https://atharva.page/post/go/go-anti-interface-everywhere/</link><pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate><guid>https://atharva.page/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></channel></rss>