<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Architecture on</title><link>/tags/architecture/</link><description>Recent content in Architecture on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 05 Nov 2025 11:46:00 +0000</lastBuildDate><atom:link href="/tags/architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 10: When Not to Use Rust — Honest trade-offs</title><link>/post/rust/rust-prod-when-not-rust/</link><pubDate>Wed, 05 Nov 2025 11:46:00 +0000</pubDate><guid>/post/rust/rust-prod-when-not-rust/</guid><description>&lt;p&gt;I like Rust. I&amp;rsquo;ve written 9 lessons about using it in production. I think it&amp;rsquo;s one of the most well-designed languages of the last twenty years. And I&amp;rsquo;m about to spend an entire article telling you when you shouldn&amp;rsquo;t use it.&lt;/p&gt;
&lt;p&gt;Because the most dangerous engineers aren&amp;rsquo;t the ones who don&amp;rsquo;t know Rust — they&amp;rsquo;re the ones who think Rust is always the answer. I&amp;rsquo;ve been that engineer. I once argued for rewriting a Flask API endpoint in Rust because &amp;ldquo;the response time was too high.&amp;rdquo; The response time was 200ms, and 180ms of that was a database query. Rust would have saved us maybe 5ms of JSON serialization. My team lead asked me to go take a walk.&lt;/p&gt;</description></item><item><title>Lesson 9: War Stories — Lessons from real Rust deployments</title><link>/post/rust/rust-prod-war-stories/</link><pubDate>Sun, 02 Nov 2025 15:08:00 +0000</pubDate><guid>/post/rust/rust-prod-war-stories/</guid><description>&lt;p&gt;Every language looks great in blog posts. Production is where the truth comes out. I&amp;rsquo;ve been running Rust services in production for a few years now, and while I&amp;rsquo;m convinced it&amp;rsquo;s the right tool for certain problems, I&amp;rsquo;ve also hit situations where Rust did something I didn&amp;rsquo;t expect, or where its strengths became weaknesses in surprising ways.&lt;/p&gt;
&lt;p&gt;These are real stories. Some names and details are changed, but the bugs and the lessons are exactly as they happened.&lt;/p&gt;</description></item><item><title>Lesson 8: Migrating Services from Go/Python/Java to Rust — When and how</title><link>/post/rust/rust-prod-migration-from-go/</link><pubDate>Thu, 30 Oct 2025 09:33:00 +0000</pubDate><guid>/post/rust/rust-prod-migration-from-go/</guid><description>&lt;p&gt;I&amp;rsquo;ve been involved in three Rust migrations. One from Python, one from Go, one from Java. Two were successes. One was a disaster that got cancelled six months in after burning a quarter of the team&amp;rsquo;s roadmap capacity.&lt;/p&gt;
&lt;p&gt;The failed one wasn&amp;rsquo;t a technical failure — the Rust code was fine. It failed because we rewrote the wrong service, at the wrong time, for the wrong reasons. &amp;ldquo;Rust is faster&amp;rdquo; was the entire justification. Nobody had measured whether speed was actually the bottleneck.&lt;/p&gt;</description></item><item><title>Lesson 7: Feature Flags at the Type Level — Compile-time feature control</title><link>/post/rust/rust-prod-feature-flags/</link><pubDate>Tue, 28 Oct 2025 13:19:00 +0000</pubDate><guid>/post/rust/rust-prod-feature-flags/</guid><description>&lt;p&gt;We had a feature that was ready for staging but absolutely not ready for production. In my previous Go gig, we&amp;rsquo;d have used a runtime feature flag service — LaunchDarkly or similar. Evaluate a boolean at request time, show the new code path to internal testers, hide it from everyone else.&lt;/p&gt;
&lt;p&gt;In Rust, we had another option. We could decide &lt;em&gt;at compile time&lt;/em&gt; whether the feature existed in the binary at all. Not a runtime check. Not a boolean. The code literally wasn&amp;rsquo;t in the production binary. You couldn&amp;rsquo;t accidentally enable it. You couldn&amp;rsquo;t exploit it. It didn&amp;rsquo;t exist.&lt;/p&gt;</description></item><item><title>Lesson 6: API Versioning and Backwards Compatibility — Don't break your users</title><link>/post/rust/rust-prod-backwards-compat/</link><pubDate>Sun, 26 Oct 2025 10:55:00 +0000</pubDate><guid>/post/rust/rust-prod-backwards-compat/</guid><description>&lt;p&gt;I once shipped a &amp;ldquo;minor&amp;rdquo; API change on a Friday. Renamed a JSON field from &lt;code&gt;user_name&lt;/code&gt; to &lt;code&gt;username&lt;/code&gt;. Seemed harmless — we were cleaning up inconsistencies. By Monday morning, we had 14 support tickets from integration partners whose parsers broke. One partner had hardcoded the field name into a system that processed payroll. People didn&amp;rsquo;t get paid because I renamed a JSON field.&lt;/p&gt;
&lt;p&gt;That was the last time I treated backwards compatibility as optional.&lt;/p&gt;</description></item><item><title>Lesson 5: Multi-Crate Workspace Architecture — Scaling your codebase</title><link>/post/rust/rust-prod-multi-crate/</link><pubDate>Thu, 23 Oct 2025 16:42:00 +0000</pubDate><guid>/post/rust/rust-prod-multi-crate/</guid><description>&lt;p&gt;Our compile times hit 8 minutes. Not from scratch — &lt;em&gt;incremental&lt;/em&gt;. Change one line in the domain model and wait 8 minutes to see if it worked. Three engineers were actively avoiding making changes to shared code because the feedback loop was unbearable.&lt;/p&gt;
&lt;p&gt;The problem was obvious: everything lived in one crate. The domain model, the HTTP handlers, the database layer, the gRPC server, the background workers — all sharing one &lt;code&gt;Cargo.toml&lt;/code&gt; with 47 dependencies. Touch anything and the whole thing recompiles.&lt;/p&gt;</description></item><item><title>Lesson 10: Interpreter Pattern — DSLs and parsing</title><link>/post/rust/rust-dp-interpreter/</link><pubDate>Wed, 22 Oct 2025 12:00:00 +0000</pubDate><guid>/post/rust/rust-dp-interpreter/</guid><description>&lt;p&gt;A few years ago I needed to let non-technical users define filtering rules for a data pipeline. The options were: embed Lua, use a YAML config with increasingly awkward syntax, or write a small domain-specific language. I picked the DSL. It took two days in Rust, and the result was a type-safe, sandboxed expression evaluator that couldn&amp;rsquo;t crash the host program no matter what users typed. Try getting that guarantee with dynamic code execution in Python.&lt;/p&gt;</description></item><item><title>Lesson 4: CQRS and Event Sourcing — Separating reads from writes</title><link>/post/rust/rust-prod-cqrs/</link><pubDate>Tue, 21 Oct 2025 08:27:00 +0000</pubDate><guid>/post/rust/rust-prod-cqrs/</guid><description>&lt;p&gt;We had this inventory service that was doing fine until it wasn&amp;rsquo;t. Reads were simple — &amp;ldquo;how many units of product X are available?&amp;rdquo; Writes were complex — reservations, adjustments, transfers between warehouses, reconciliation with physical counts. Both read and write operations hit the same database table, the same data model, and the same set of queries that were getting increasingly gnarly.&lt;/p&gt;
&lt;p&gt;Then we hit Black Friday. Read traffic spiked 40x. The complex write queries were locking rows that the read queries needed. We couldn&amp;rsquo;t scale reads without scaling writes. We couldn&amp;rsquo;t optimize the read path without breaking the write path&amp;rsquo;s invariants.&lt;/p&gt;</description></item><item><title>Lesson 9: Entity Component System — Data-oriented design</title><link>/post/rust/rust-dp-ecs/</link><pubDate>Sun, 19 Oct 2025 15:30:00 +0000</pubDate><guid>/post/rust/rust-dp-ecs/</guid><description>&lt;p&gt;I spent years thinking about game objects the OOP way. A &lt;code&gt;Player&lt;/code&gt; extends &lt;code&gt;Character&lt;/code&gt; extends &lt;code&gt;Entity&lt;/code&gt;. A &lt;code&gt;Goblin&lt;/code&gt; extends &lt;code&gt;Enemy&lt;/code&gt; extends &lt;code&gt;Character&lt;/code&gt; extends &lt;code&gt;Entity&lt;/code&gt;. Then someone asks &amp;ldquo;what if a goblin can be mind-controlled and act like a player?&amp;rdquo; and your inheritance hierarchy collapses.&lt;/p&gt;
&lt;p&gt;ECS — Entity Component System — is the answer that the game development world converged on, and it&amp;rsquo;s fundamentally a Rust-shaped idea. Data and behavior are separated. Composition replaces inheritance. Cache-friendly memory layouts replace pointer-chasing object graphs. Rust&amp;rsquo;s ownership model maps onto ECS so naturally that Bevy — the most popular Rust game engine — is built entirely around it.&lt;/p&gt;</description></item><item><title>Lesson 3: Hexagonal Architecture in Rust — Ports, adapters, and boundaries</title><link>/post/rust/rust-prod-hexagonal/</link><pubDate>Sun, 19 Oct 2025 11:05:00 +0000</pubDate><guid>/post/rust/rust-prod-hexagonal/</guid><description>&lt;p&gt;About a year ago, I had to swap out our payment provider. In Go, that would&amp;rsquo;ve been a two-week project — chasing down every place we called Stripe&amp;rsquo;s SDK, updating structs, fixing test mocks. In our Rust service, it took a day and a half. The reason wasn&amp;rsquo;t Rust itself. It was how we&amp;rsquo;d structured the code.&lt;/p&gt;
&lt;p&gt;Hexagonal architecture (sometimes called &amp;ldquo;ports and adapters&amp;rdquo;) is one of those patterns that sounds academic until you actually need to replace a database, swap a message broker, or test your business logic without spinning up Docker containers. In Rust, traits make it feel natural rather than ceremonial.&lt;/p&gt;</description></item><item><title>Lesson 2: Domain Modeling with Rust's Type System — Making impossible states impossible</title><link>/post/rust/rust-prod-domain-modeling/</link><pubDate>Fri, 17 Oct 2025 14:38:00 +0000</pubDate><guid>/post/rust/rust-prod-domain-modeling/</guid><description>&lt;p&gt;We shipped a bug to production that cost us about three hours of incident response and a very uncomfortable Slack thread. The root cause? Someone passed a &lt;code&gt;user_id&lt;/code&gt; where an &lt;code&gt;order_id&lt;/code&gt; was expected. Both were &lt;code&gt;String&lt;/code&gt;. Both were UUIDs. The compiler had no way to tell them apart. The function signature said &lt;code&gt;fn cancel_order(order_id: String, user_id: String)&lt;/code&gt;, and someone called it with the arguments flipped.&lt;/p&gt;
&lt;p&gt;This is the kind of bug that makes you rethink everything. Not because it&amp;rsquo;s complex — because it&amp;rsquo;s &lt;em&gt;stupid&lt;/em&gt;. And stupid bugs that slip through a strong type system mean the type system wasn&amp;rsquo;t being used right.&lt;/p&gt;</description></item><item><title>Lesson 8: Middleware / Chain of Responsibility — Tower-style</title><link>/post/rust/rust-dp-middleware/</link><pubDate>Thu, 16 Oct 2025 07:15:00 +0000</pubDate><guid>/post/rust/rust-dp-middleware/</guid><description>&lt;p&gt;If you&amp;rsquo;ve built anything with Express, Koa, or ASP.NET, you know middleware. A request comes in, passes through a chain of handlers — logging, auth, rate limiting, CORS — and eventually reaches your application logic. Each handler can modify the request, short-circuit the chain, or pass it along.&lt;/p&gt;
&lt;p&gt;The Chain of Responsibility pattern from the GoF book is basically the same thing. The difference is branding.&lt;/p&gt;
&lt;p&gt;What makes this pattern interesting in Rust is &lt;code&gt;tower&lt;/code&gt; — the crate that defines how middleware works across the entire Rust async ecosystem. Axum, Tonic, Hyper — they all use Tower&amp;rsquo;s &lt;code&gt;Service&lt;/code&gt; trait. Understanding it unlocks the middleware patterns in all of these frameworks.&lt;/p&gt;</description></item><item><title>Lesson 1: Structuring a Large Rust Application — Beyond hello world</title><link>/post/rust/rust-prod-architecture/</link><pubDate>Wed, 15 Oct 2025 09:14:00 +0000</pubDate><guid>/post/rust/rust-prod-architecture/</guid><description>&lt;p&gt;The moment I knew our Rust project structure was broken was when a junior engineer asked me where to put a new endpoint. I opened the repo, stared at the &lt;code&gt;src/&lt;/code&gt; directory, and realized I couldn&amp;rsquo;t confidently answer. We had 40,000 lines of Rust spread across files with names like &lt;code&gt;utils.rs&lt;/code&gt;, &lt;code&gt;helpers.rs&lt;/code&gt;, &lt;code&gt;types.rs&lt;/code&gt;, and the ever-popular &lt;code&gt;misc.rs&lt;/code&gt;. Everything compiled. Nothing made sense.&lt;/p&gt;
&lt;p&gt;Most Rust tutorials stop at &amp;ldquo;put your code in &lt;code&gt;main.rs&lt;/code&gt; and maybe &lt;code&gt;lib.rs&lt;/code&gt;.&amp;rdquo; That works for a CLI tool or a weekend project. It completely falls apart when you&amp;rsquo;ve got a team of eight engineers building a platform with multiple services, shared domain logic, and infrastructure that&amp;rsquo;s evolving every sprint.&lt;/p&gt;</description></item><item><title>Lesson 7: Repository Pattern — Abstracting storage</title><link>/post/rust/rust-dp-repository/</link><pubDate>Mon, 13 Oct 2025 13:45:00 +0000</pubDate><guid>/post/rust/rust-dp-repository/</guid><description>&lt;p&gt;Every backend developer eventually writes the same code: a function that takes a database connection, runs a query, maps the rows to a struct, and returns it. Then you write another one. And another. Pretty soon your business logic is tangled up with SQL strings and connection pool handles, and testing anything requires a running database.&lt;/p&gt;
&lt;p&gt;The Repository pattern fixes this. It&amp;rsquo;s old — Martin Fowler wrote about it in 2002 — but the way Rust implements it is genuinely different from what you&amp;rsquo;d do in Java or C#. Rust&amp;rsquo;s trait system, combined with generics and lifetimes, gives you a repository abstraction that&amp;rsquo;s zero-cost in production and trivially mockable in tests.&lt;/p&gt;</description></item><item><title>Lesson 6: Factory Patterns — When constructors aren't enough</title><link>/post/rust/rust-dp-factory/</link><pubDate>Sat, 11 Oct 2025 10:00:00 +0000</pubDate><guid>/post/rust/rust-dp-factory/</guid><description>&lt;p&gt;Here&amp;rsquo;s a hot take: most &amp;ldquo;Factory pattern&amp;rdquo; usage in Java and C# exists purely to work around limitations of constructors. Constructors can&amp;rsquo;t have descriptive names. They can&amp;rsquo;t return a different subtype. They can&amp;rsquo;t fail gracefully. So you wrap them in a static method and call it a Factory.&lt;/p&gt;
&lt;p&gt;Rust doesn&amp;rsquo;t have constructors at all. Every struct is constructed directly, and associated functions already do what Factory Method does in OOP. So do you even need the Factory pattern in Rust?&lt;/p&gt;</description></item><item><title>Lesson 5: Decorator Pattern — Wrapping with trait composition</title><link>/post/rust/rust-dp-decorator/</link><pubDate>Thu, 09 Oct 2025 16:30:00 +0000</pubDate><guid>/post/rust/rust-dp-decorator/</guid><description>&lt;p&gt;I remember the moment Decorator clicked for me. I was reading the source for Java&amp;rsquo;s I/O library — &lt;code&gt;BufferedInputStream&lt;/code&gt; wrapping &lt;code&gt;FileInputStream&lt;/code&gt; wrapping &lt;code&gt;InputStream&lt;/code&gt;. Three layers deep, each adding behavior without subclassing. Elegant. Then I tried to write the same thing in Rust and learned that &amp;ldquo;wrapping a thing while preserving its interface&amp;rdquo; is a fundamentally different exercise when you don&amp;rsquo;t have inheritance.&lt;/p&gt;
&lt;p&gt;The good news? Rust&amp;rsquo;s version is often &lt;em&gt;better&lt;/em&gt; than the OOP original, because composition is the default and the compiler enforces the contracts.&lt;/p&gt;</description></item><item><title>Lesson 4: Command Pattern — Closures as commands</title><link>/post/rust/rust-dp-command/</link><pubDate>Tue, 07 Oct 2025 09:20:00 +0000</pubDate><guid>/post/rust/rust-dp-command/</guid><description>&lt;p&gt;The Command pattern has always struck me as one of those patterns that&amp;rsquo;s really just &amp;ldquo;wrap a function call in an object.&amp;rdquo; In Java, you create a &lt;code&gt;Command&lt;/code&gt; interface with an &lt;code&gt;execute()&lt;/code&gt; method, make a class for each command, and pass them around. It takes about 40 lines of boilerplate to do what a lambda does in one.&lt;/p&gt;
&lt;p&gt;In Rust, closures &lt;em&gt;are&lt;/em&gt; the Command pattern. But when you need undo, history, or serialization, you still need the structured version — and Rust&amp;rsquo;s ownership model makes the undo/redo part surprisingly elegant.&lt;/p&gt;</description></item><item><title>Lesson 3: Observer Pattern — Channels and callbacks in Rust</title><link>/post/rust/rust-dp-observer/</link><pubDate>Sun, 05 Oct 2025 11:45:00 +0000</pubDate><guid>/post/rust/rust-dp-observer/</guid><description>&lt;p&gt;The Observer pattern is where Rust&amp;rsquo;s ownership model gets &lt;em&gt;really&lt;/em&gt; opinionated. In C# or Java, Observer is simple — maintain a list of listeners, call a method on each when something happens. But &amp;ldquo;a list of mutable references to objects that can be called at any time&amp;rdquo; is basically everything Rust&amp;rsquo;s borrow checker exists to prevent. So how do you do event-driven programming in Rust? You have more options than you&amp;rsquo;d think, and some of them are better than what OOP languages offer.&lt;/p&gt;</description></item><item><title>Lesson 2: Strategy Pattern — Trait objects and generics</title><link>/post/rust/rust-dp-strategy/</link><pubDate>Fri, 03 Oct 2025 14:15:00 +0000</pubDate><guid>/post/rust/rust-dp-strategy/</guid><description>&lt;p&gt;In my first real Go project, I wrote an interface for a payment processor. Two implementations — Stripe and PayPal. Simple polymorphism. When I tried the same thing in Rust, the compiler hit me with a wall of errors about &lt;code&gt;dyn&lt;/code&gt;, &lt;code&gt;Box&lt;/code&gt;, object safety, and sized types. It took me a full afternoon to understand what was happening. The Strategy pattern — which is trivial in most OOP languages — forced me to actually understand Rust&amp;rsquo;s type system. And I came out the other side a better programmer for it.&lt;/p&gt;</description></item><item><title>Lesson 1: Builder Pattern — Typestate builders and compile-time validation</title><link>/post/rust/rust-dp-builder-advanced/</link><pubDate>Wed, 01 Oct 2025 08:30:00 +0000</pubDate><guid>/post/rust/rust-dp-builder-advanced/</guid><description>&lt;p&gt;I once spent three days debugging a production outage caused by a builder that silently accepted a missing &lt;code&gt;host&lt;/code&gt; field and defaulted to &lt;code&gt;localhost&lt;/code&gt;. In Java. The builder compiled fine, the tests passed — because they ran against localhost — and the deployment connected to nothing. That was the day I stopped trusting optional fields in builders.&lt;/p&gt;
&lt;p&gt;Rust&amp;rsquo;s type system lets you make that entire category of bug impossible. Not at runtime. Not with validation methods. At &lt;em&gt;compile time&lt;/em&gt;.&lt;/p&gt;</description></item><item><title>Lesson 8: Monolith-First — Modular monoliths in Rust</title><link>/post/rust/rust-micro-monolith-first/</link><pubDate>Tue, 17 Jun 2025 13:19:00 +0000</pubDate><guid>/post/rust/rust-micro-monolith-first/</guid><description>&lt;p&gt;I&amp;rsquo;m going to tell you something that might sound weird after seven lessons about microservices patterns: don&amp;rsquo;t start with microservices. Start with a monolith. A well-structured, modular monolith that&amp;rsquo;s &lt;em&gt;designed&lt;/em&gt; to be split later.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t contrarianism for its own sake. I&amp;rsquo;ve seen three teams build microservices from day one. All three regretted it. One team spent more time debugging distributed system issues than building features. Another had seven services that each handled about 50 requests per day — the infrastructure cost was absurd. The third discovered six months in that they&amp;rsquo;d drawn their service boundaries wrong and had to do a painful re-architecture.&lt;/p&gt;</description></item><item><title>Lesson 7: Testing Microservices — Contract tests and integration</title><link>/post/rust/rust-micro-testing/</link><pubDate>Sat, 14 Jun 2025 07:33:00 +0000</pubDate><guid>/post/rust/rust-micro-testing/</guid><description>&lt;p&gt;I deployed a change to the order service that renamed a field from &lt;code&gt;total_amount&lt;/code&gt; to &lt;code&gt;total_cents&lt;/code&gt;. Made perfect sense — cents avoid floating-point nonsense. All unit tests passed. Integration tests passed. Staging looked fine.&lt;/p&gt;
&lt;p&gt;Production broke instantly. The payment service was still expecting &lt;code&gt;total_amount&lt;/code&gt;. It deserialized the response, got &lt;code&gt;None&lt;/code&gt; for the amount, and started processing $0 charges. We caught it in four minutes, but four minutes of free orders adds up.&lt;/p&gt;</description></item><item><title>Lesson 6: Distributed Tracing Across Services — Following requests</title><link>/post/rust/rust-micro-tracing/</link><pubDate>Wed, 11 Jun 2025 10:45:00 +0000</pubDate><guid>/post/rust/rust-micro-tracing/</guid><description>&lt;p&gt;&amp;ldquo;It&amp;rsquo;s slow&amp;rdquo; is the most useless bug report in a microservices world. Slow where? The API gateway? The order service? The database query inside the payment service? The message queue between inventory and shipping? When a single user action touches five services and three databases, &amp;ldquo;it&amp;rsquo;s slow&amp;rdquo; could mean anything.&lt;/p&gt;
&lt;p&gt;I spent an entire afternoon once trying to track down a latency spike. Added timing logs to every service. Correlated timestamps across hosts. Manually stitched together the request flow from six different log streams. Found the culprit: a DNS resolution that was taking 800ms because of a misconfigured resolver — in a service I didn&amp;rsquo;t even know was involved.&lt;/p&gt;</description></item><item><title>Lesson 5: Service Mesh Integration — Istio, Linkerd, and Rust</title><link>/post/rust/rust-micro-service-mesh/</link><pubDate>Mon, 09 Jun 2025 16:21:00 +0000</pubDate><guid>/post/rust/rust-micro-service-mesh/</guid><description>&lt;p&gt;I&amp;rsquo;ll be honest — when someone first pitched &amp;ldquo;service mesh&amp;rdquo; to me, I thought it was over-engineered marketing. You&amp;rsquo;re telling me I need a sidecar proxy bolted onto every pod, a control plane to manage those proxies, and custom CRDs to configure traffic routing&amp;hellip; just to do what a load balancer and some retry logic could handle?&lt;/p&gt;
&lt;p&gt;Then I ran a fleet of 20+ services in production. mTLS between everything? Doing that in application code is painful. Per-route retry policies? Circuit breaking with consistent configuration? Gradual traffic shifting for canary deploys? At that scale, doing it all in app code means doing it differently in every service, with different bugs in each implementation.&lt;/p&gt;</description></item><item><title>Lesson 4: Saga Pattern — Distributed transactions without 2PC</title><link>/post/rust/rust-micro-saga/</link><pubDate>Sat, 07 Jun 2025 08:48:00 +0000</pubDate><guid>/post/rust/rust-micro-saga/</guid><description>&lt;p&gt;Here&amp;rsquo;s a scenario that&amp;rsquo;ll ruin your week. A customer places an order. Your order service saves it. Your payment service charges their card. Your inventory service reserves the items. Your shipping service schedules a pickup. Then the shipping service discovers the item is oversized and can&amp;rsquo;t be shipped to that address.&lt;/p&gt;
&lt;p&gt;Now what? The card&amp;rsquo;s been charged. The inventory&amp;rsquo;s been reserved. The order exists. You need to undo three things across three services, each with their own database, each with their own failure modes. Welcome to distributed transactions.&lt;/p&gt;</description></item><item><title>Lesson 3: Event-Driven Architecture in Rust — Decoupled systems</title><link>/post/rust/rust-micro-event-driven/</link><pubDate>Thu, 05 Jun 2025 11:02:00 +0000</pubDate><guid>/post/rust/rust-micro-event-driven/</guid><description>&lt;p&gt;The worst production incident I ever dealt with was a cascading failure triggered by a single slow database query. Service A called Service B synchronously, which called Service C, which ran a query that usually took 2ms but on that particular Tuesday took 45 seconds because of a missing index on a new column. Service A&amp;rsquo;s thread pool exhausted, its health check failed, Kubernetes restarted it, and for twenty minutes the entire checkout flow was down — because of a read query in a recommendation engine.&lt;/p&gt;</description></item><item><title>Lesson 2: gRPC Microservices with tonic — Production-grade RPC</title><link>/post/rust/rust-micro-grpc-services/</link><pubDate>Tue, 03 Jun 2025 14:37:00 +0000</pubDate><guid>/post/rust/rust-micro-grpc-services/</guid><description>&lt;p&gt;The first time I used gRPC in production was on a Go project. The experience was fine — &lt;code&gt;protoc&lt;/code&gt; generated stubs, you implemented an interface, done. Then I tried &lt;code&gt;tonic&lt;/code&gt; in Rust, and I realized what gRPC was &lt;em&gt;supposed&lt;/em&gt; to feel like. Type-safe request/response types generated at compile time, streaming that works with Rust&amp;rsquo;s async model, and interceptors built on the same Tower middleware stack as Axum. It&amp;rsquo;s gRPC done right.&lt;/p&gt;</description></item><item><title>Lesson 1: Service Boundaries and API Contracts — Where to draw the lines</title><link>/post/rust/rust-micro-service-design/</link><pubDate>Sun, 01 Jun 2025 09:14:00 +0000</pubDate><guid>/post/rust/rust-micro-service-design/</guid><description>&lt;p&gt;I once joined a team that had 47 microservices for what was essentially a CRUD app with a payment flow. Forty-seven. Each one had its own database, its own deployment pipeline, its own on-call rotation. When a customer placed an order, the request bounced through eleven services before a confirmation email went out. Latency was terrible, debugging was a nightmare, and nobody could explain why the &amp;ldquo;UserPreferences&amp;rdquo; service existed separately from the &amp;ldquo;UserProfile&amp;rdquo; service.&lt;/p&gt;</description></item><item><title>Lesson 7: go.work and Workspace Mode — Developing multiple modules without replace hacks</title><link>/post/go/go-pkg-workspaces/</link><pubDate>Tue, 25 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-workspaces/</guid><description>&lt;p&gt;Before &lt;code&gt;go.work&lt;/code&gt; existed, developing across multiple local Go modules was genuinely painful. You were working on a library in one directory and an application that depended on it in another. Every time you changed the library, you had to add a &lt;code&gt;replace&lt;/code&gt; directive to the application&amp;rsquo;s &lt;code&gt;go.mod&lt;/code&gt;, run your tests, and then remember — always remember — to remove the &lt;code&gt;replace&lt;/code&gt; before committing. I have seen &lt;code&gt;replace&lt;/code&gt; directives committed to production &lt;code&gt;go.mod&lt;/code&gt; files more times than I would like to admit.&lt;/p&gt;</description></item><item><title>Lesson 6: Dependency Direction — Always depend inward, never outward</title><link>/post/go/go-pkg-dependency-direction/</link><pubDate>Thu, 02 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-dependency-direction/</guid><description>&lt;p&gt;We fixed cycles in lesson 2. But removing cycles is a necessary condition for good architecture, not a sufficient one. You can have a perfectly cycle-free dependency graph where every arrow points in the wrong direction, and the result is still a system that is painful to test, extend, and reason about.&lt;/p&gt;
&lt;p&gt;Dependency direction is about more than just &amp;ldquo;does A import B.&amp;rdquo; It is about which layer owns the core business rules and which layers are allowed to know about which other layers. Get this wrong and your domain logic ends up tangled with your database driver. Get it right and you can replace your entire HTTP layer, or swap Postgres for a different store, without changing a single line of business logic.&lt;/p&gt;</description></item><item><title>Lesson 3: CQRS + Event Sourcing Together — When the combination makes sense</title><link>/post/fundamentals/es-cqrs-together/</link><pubDate>Sun, 24 Nov 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/es-cqrs-together/</guid><description>&lt;p&gt;CQRS and event sourcing are often discussed together, presented as a single package, and that conflation is responsible for a lot of unnecessary complexity in codebases that should have stayed simple. I&amp;rsquo;ve seen teams adopt both because they read a blog post, without being clear on why they needed either. I&amp;rsquo;ve also seen teams who should have used both and instead built elaborate workarounds that were essentially CQRS and event sourcing with worse ergonomics. The question isn&amp;rsquo;t &amp;ldquo;should I use CQRS and event sourcing?&amp;rdquo; — it&amp;rsquo;s &amp;ldquo;do I have the specific problems these patterns solve?&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 5: Monolith vs Multi-Module — One go.mod or many? It depends.</title><link>/post/go/go-pkg-mono-vs-multi/</link><pubDate>Fri, 08 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-mono-vs-multi/</guid><description>&lt;p&gt;When I first started structuring larger Go projects, I defaulted to what felt natural: one repository, one &lt;code&gt;go.mod&lt;/code&gt;. It worked for a long time. Then I joined a project with four services in a single repo, each with different dependency requirements, and suddenly the single &lt;code&gt;go.mod&lt;/code&gt; was pulling in every dependency of every service for every build. Tests for the email service were slow because the &lt;code&gt;go test&lt;/code&gt; run was loading the ML inference library needed only by the recommendation service. That was when I started thinking seriously about module boundaries, not just package boundaries.&lt;/p&gt;</description></item><item><title>Lesson 4: Interface Placement — Define interfaces where they're used, not where they're implemented</title><link>/post/go/go-pkg-interface-placement/</link><pubDate>Sat, 28 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-interface-placement/</guid><description>&lt;p&gt;This is the Go idiom that surprises people coming from Java or C# the most. In those languages, you define an interface in the same place as (or even before) the implementation, and consumers import the interface. Go&amp;rsquo;s approach is the exact opposite, and it takes a while to internalize why. Once it clicks, it changes how you think about dependencies fundamentally.&lt;/p&gt;
&lt;p&gt;The rule is: define an interface in the package that needs it, not in the package that satisfies it. It sounds backwards. It is not. It is one of the most powerful design decisions the language enables.&lt;/p&gt;</description></item><item><title>Lesson 2: Projections and Read Models — Build any view from your event stream</title><link>/post/fundamentals/es-projections/</link><pubDate>Fri, 13 Sep 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/es-projections/</guid><description>&lt;p&gt;After I shipped the event-sourced account system, the first question from the product team was: &amp;ldquo;Can we have a page that shows all accounts that have been dormant for more than 90 days?&amp;rdquo; In a traditional system, this is a query: &lt;code&gt;SELECT * FROM accounts WHERE last_activity &amp;lt; NOW() - INTERVAL '90 days'&lt;/code&gt;. In event sourcing, there&amp;rsquo;s no &lt;code&gt;last_activity&lt;/code&gt; column — there&amp;rsquo;s an event stream. My first instinct was to query the event store directly, find the latest event per stream, and filter. It worked. Then they asked for the top 100 accounts by balance. Then active accounts by geography. Then a real-time dashboard with all of the above simultaneously. Querying the event store for each of these is either very slow or very complex. The answer is projections — pre-computed read models built from your event streams.&lt;/p&gt;</description></item><item><title>Lesson 8: Technical Debt — When to pay, when to live with it</title><link>/post/fundamentals/arch-tech-debt/</link><pubDate>Fri, 23 Aug 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-tech-debt/</guid><description>&lt;p&gt;The term &amp;ldquo;technical debt&amp;rdquo; gets used to mean everything from &amp;ldquo;this code is a mess&amp;rdquo; to &amp;ldquo;we made a pragmatic shortcut we need to revisit&amp;rdquo; to &amp;ldquo;this system has grown organically and nobody understands it anymore.&amp;rdquo; These are different problems requiring different responses. Treating all technical debt as something that must be paid now, or all of it as something acceptable to defer indefinitely — both lead to bad outcomes. The skill is knowing which debt to address, when, and how.&lt;/p&gt;</description></item><item><title>Lesson 3: internal/ Usage Patterns — Compiler-enforced encapsulation</title><link>/post/go/go-pkg-internal/</link><pubDate>Sun, 18 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-internal/</guid><description>&lt;p&gt;Go does not have access modifiers in the Java or Kotlin sense. There is no &lt;code&gt;protected&lt;/code&gt;, no &lt;code&gt;package-private&lt;/code&gt;, no friend classes. You get exactly two visibility levels: exported (capital letter) and unexported (lowercase letter). That simplicity is a feature. But it creates a gap: how do you share something across packages within your own module without accidentally exposing it to the outside world?&lt;/p&gt;
&lt;p&gt;The answer is &lt;code&gt;internal/&lt;/code&gt;. It is one of the most underused and underappreciated tools in Go&amp;rsquo;s package system, and it is enforced by the compiler itself.&lt;/p&gt;</description></item><item><title>Lesson 7: Migration Strategies — Strangler fig and feature flags</title><link>/post/fundamentals/arch-migrations/</link><pubDate>Wed, 07 Aug 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-migrations/</guid><description>&lt;p&gt;Rewrites are seductive. The existing system is messy, it&amp;rsquo;s slow to change, and the new system in your head is clean and fast and well-designed. Then you start the rewrite. Six months in, you&amp;rsquo;ve rebuilt 40% of the functionality and the remaining 60% is more complex than you thought. Meanwhile, the old system keeps shipping features. The new system falls behind, gets cancelled, and you&amp;rsquo;re back where you started, except now you&amp;rsquo;ve lost six months and the team is demoralized. I&amp;rsquo;ve seen this happen twice. The third time, we used the strangler fig pattern instead, and it worked.&lt;/p&gt;</description></item><item><title>Lesson 6: API Versioning — URL, header, or content negotiation</title><link>/post/fundamentals/arch-api-versioning/</link><pubDate>Wed, 24 Jul 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-api-versioning/</guid><description>&lt;p&gt;The first time I shipped a breaking change to a production API without a version, I got an incident at 2am because a partner integration stopped working. They&amp;rsquo;d been calling &lt;code&gt;/api/orders&lt;/code&gt; for six months. I changed the response shape. Their code broke. That was when &amp;ldquo;API versioning&amp;rdquo; moved from &amp;ldquo;thing to do eventually&amp;rdquo; to &amp;ldquo;thing to do before you ship anything external.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no universally right answer to how you version APIs. There are tradeoffs, and the choice you make will shape your codebase for years. Here&amp;rsquo;s how I think about it.&lt;/p&gt;</description></item><item><title>Lesson 2: Avoiding Cyclic Dependencies — If packages import each other, your design is wrong</title><link>/post/go/go-pkg-cyclic-deps/</link><pubDate>Fri, 12 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-cyclic-deps/</guid><description>&lt;p&gt;The Go compiler refuses to build a program with a cyclic import. It is not a warning. It is not a lint violation. It is a hard failure. I used to find this annoying when I was newer to the language. Now I consider it one of Go&amp;rsquo;s greatest gifts. The compiler is telling you something important: if package A needs package B and package B needs package A, you have not yet understood what the relationship between these two concepts really is.&lt;/p&gt;</description></item><item><title>Lesson 5: DDD Essentials — Bounded contexts and aggregates</title><link>/post/fundamentals/arch-ddd/</link><pubDate>Sun, 07 Jul 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-ddd/</guid><description>&lt;p&gt;The concept of &amp;ldquo;customer&amp;rdquo; meant something different in every team I talked to at one company. To the billing team, a customer was a billing account. To the support team, a customer was a person who filed tickets. To the identity team, a customer was an authenticated principal. Every team had their own &lt;code&gt;Customer&lt;/code&gt; struct, and syncing them was a full-time job. That&amp;rsquo;s the problem DDD&amp;rsquo;s bounded context concept solves — and it&amp;rsquo;s one of those ideas that sounds academic until you&amp;rsquo;ve suffered without it.&lt;/p&gt;</description></item><item><title>Lesson 1: Event Store Design — Append-only logs that are your source of truth</title><link>/post/fundamentals/es-event-store/</link><pubDate>Sat, 29 Jun 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/es-event-store/</guid><description>&lt;p&gt;My first encounter with event sourcing was a financial system that needed a complete audit trail. The business requirement was clear: every change to an account balance had to be traceable — who changed it, when, why. The first approach was an audit log table alongside the main accounts table. We&amp;rsquo;d write to both in a transaction. Within six months, the audit table was out of sync with the accounts table. Bugs in the dual-write logic, a migration that updated account records without touching the audit log, a direct database fix that bypassed the application layer. The audit log was nearly useless. The core insight I eventually arrived at: the audit log shouldn&amp;rsquo;t be a secondary record — it should be the primary record. That&amp;rsquo;s event sourcing.&lt;/p&gt;</description></item><item><title>Lesson 4: CQRS — When reads and writes need different models</title><link>/post/fundamentals/arch-cqrs/</link><pubDate>Fri, 21 Jun 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-cqrs/</guid><description>&lt;p&gt;I maintained an e-commerce admin dashboard that had a query so complex it took 12 seconds to run. It joined seven tables across the orders, inventory, and customers domains to build a summary view of &amp;ldquo;all orders pending fulfillment, with customer tier, item details, and warehouse stock levels.&amp;rdquo; Every time the admin loaded the page, twelve seconds. We tried indexes, caching, materialized views — all helped at the margins. The fundamental problem was that we were asking our write-optimized relational model to answer a read-optimized reporting question. Separating the read model was the only real fix.&lt;/p&gt;</description></item><item><title>Lesson 3: Event-Driven Architecture — Events vs commands</title><link>/post/fundamentals/arch-event-driven/</link><pubDate>Tue, 04 Jun 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-event-driven/</guid><description>&lt;p&gt;I used to use &amp;ldquo;event&amp;rdquo; and &amp;ldquo;command&amp;rdquo; interchangeably. A message is a message, right? Then I started debugging an event-driven system where the payment service was publishing &lt;code&gt;ProcessPayment&lt;/code&gt; events, the order service was publishing &lt;code&gt;CreateShipment&lt;/code&gt; events, and two teams were arguing about who owned the workflow. The problem was naming: they were publishing commands disguised as events. The distinction isn&amp;rsquo;t pedantic — it determines who owns the workflow and how the system evolves.&lt;/p&gt;</description></item><item><title>Lesson 1: Package Boundaries — One package, one responsibility, zero excuses</title><link>/post/go/go-pkg-boundaries/</link><pubDate>Mon, 03 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-boundaries/</guid><description>&lt;p&gt;I have refactored a lot of Go codebases — my own included. And the single most common structural mistake I see is the &lt;code&gt;utils&lt;/code&gt; package. Or &lt;code&gt;helpers&lt;/code&gt;. Or &lt;code&gt;common&lt;/code&gt;. Or sometimes the worst offender of all: a package named after the entire application domain with fifty unrelated files sitting inside it. When I first started writing Go seriously, I made all of these mistakes. This lesson is about why package boundaries matter and how to get them right.&lt;/p&gt;</description></item><item><title>Lesson 2: Clean Architecture — Not the textbook version</title><link>/post/fundamentals/arch-clean/</link><pubDate>Sun, 19 May 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-clean/</guid><description>&lt;p&gt;I&amp;rsquo;ve read the Clean Architecture book. I&amp;rsquo;ve also seen teams implement it so literally that they had six layers of indirection for a CRUD endpoint: a controller called a use case, which called a domain service, which called a port, which went through an adapter, which called a repository, which hit the database. Each hop had its own error mapping. Changing a database column required touching eight files. That&amp;rsquo;s not clean. That&amp;rsquo;s engineering theater.&lt;/p&gt;</description></item><item><title>Lesson 1: Monolith First — Starting with microservices is usually wrong</title><link>/post/fundamentals/arch-monolith-first/</link><pubDate>Fri, 03 May 2024 00:00:00 +0000</pubDate><guid>/post/fundamentals/arch-monolith-first/</guid><description>&lt;p&gt;I&amp;rsquo;ve seen two teams start new products with microservices. One spent four months before they had anything deployed — Kubernetes setup, service discovery, distributed tracing, a CI/CD pipeline for twelve repos, and debates about how to split domains they hadn&amp;rsquo;t fully understood yet. They ran out of runway. The other team I worked on started with a monolith, shipped their first real user feature in three weeks, and only extracted services when the actual pain of growth made the benefit obvious. We&amp;rsquo;re still running that system, and it handles tens of millions of requests a day.&lt;/p&gt;</description></item></channel></rss>