<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Daniel Hidalgo&apos;s blog</title><description>Backend architecture, system design and engineering leadership.</description><link>https://hrdi.dev/</link><language>en</language><item><title>One AI Setup, Two Worlds: How I Use AI Agents as a Tech Lead and as an Entrepreneur</title><link>https://hrdi.dev/blog/ai-setup-work-and-business/</link><guid isPermaLink="true">https://hrdi.dev/blog/ai-setup-work-and-business/</guid><description>The actual setup I run every day on my machine, how it adapts between my job as a Tech Lead and my own company, and the rules that keep AI useful instead of dangerous.</description><pubDate>Fri, 23 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I wrote before about &lt;a href=&quot;/blog/ai-developer-workflow/&quot;&gt;what actually works with AI in the developer workflow&lt;/a&gt;. That post was about principles. This one is about the setup itself: the tools that run on my machine every day, how the same setup serves two very different contexts, and the rules I follow so that it stays a tool and not a liability.&lt;/p&gt;
&lt;p&gt;The two contexts are my day job as a Tech Lead and Arupo, the company I&apos;m building as a partner, plus a handful of personal projects. Very different constraints, one engineer, one laptop.&lt;/p&gt;
&lt;h2&gt;The Foundation: Configure Once, Use Everywhere&lt;/h2&gt;
&lt;p&gt;The biggest mistake I see is treating every AI tool as a separate island: one set of instructions in one editor, different habits in another, nothing shared between them. I went the other way. There is one foundation, and every agent sits on top of it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A shared harness.&lt;/strong&gt; I use &lt;a href=&quot;https://github.com/Gentleman-Programming/gentle-ai&quot;&gt;Gentle AI&lt;/a&gt; to give every agent the same workflow, the same persona and the same review rules. It&apos;s configured for Claude Code, Codex, OpenCode, Gemini and Copilot. Whichever agent I open, it works the same way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent memory.&lt;/strong&gt; &lt;a href=&quot;https://github.com/Gentleman-Programming/engram&quot;&gt;Engram&lt;/a&gt; stores decisions, bug fixes, discoveries and preferences across sessions and across agents. When I come back to a project after two weeks, the agent doesn&apos;t start from zero, and neither do I.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A code map.&lt;/strong&gt; Codegraph indexes each repository so an agent can understand the structure without reading file by file. That matters when the codebase is large and the context window is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current documentation.&lt;/strong&gt; Context7 gives agents the up-to-date docs of the libraries I use, instead of whatever version they memorized during training.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A second AI terminal.&lt;/strong&gt; Pi, with the Gentle Shell, is where I run longer orchestrated work, with profiles for different models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A guardian on commits.&lt;/strong&gt; A pre-commit reviewer checks changes before they land.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A comfortable environment.&lt;/strong&gt; Ghostty, Starship and the Gentleman dotfiles. It sounds cosmetic, but you spend the whole day there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these is magic on its own. The value is that they are wired together: the same rules, the same memory, the same way of working, regardless of the agent or the project.&lt;/p&gt;
&lt;h2&gt;Two Worlds on One Machine&lt;/h2&gt;
&lt;p&gt;Here is the part people rarely talk about. My job and my company must never mix. Different accounts, different identities, different rules about what can leave the machine.&lt;/p&gt;
&lt;p&gt;So the separation is automatic, by folder. When I enter my personal projects folder, the personal AI profile and my personal git and SSH identity activate on their own. When I enter the work folder, the corporate AI profile and the corporate git identity take over. I don&apos;t have to remember to switch, and that is the point: a rule you have to remember is a rule you will eventually break.&lt;/p&gt;
&lt;p&gt;It isn&apos;t perfect, and I know where the gaps are. The GitHub CLI, for example, doesn&apos;t switch accounts per folder; it uses one global session. Knowing the gaps of your own setup is part of owning it.&lt;/p&gt;
&lt;h2&gt;At Work: Leading Inside the Constraints&lt;/h2&gt;
&lt;p&gt;A company has rules about tools and data, and they exist for good reasons. My rule is simple: the way of working travels with me, the company&apos;s policies decide which tools I can use. I adapt the setup to fit those policies, never the other way around.&lt;/p&gt;
&lt;p&gt;But the bigger change at work isn&apos;t technical. It&apos;s how I spend my time as a Tech Lead. Agents take a lot of the mechanical work: exploring an unfamiliar service, drafting a migration, writing the boring tests. That gives me more time for what a Tech Lead is actually for: design decisions, code reviews, unblocking the team, incidents and postmortems.&lt;/p&gt;
&lt;p&gt;One thing does not change: &lt;strong&gt;ownership stays human&lt;/strong&gt;. An agent can propose a change to a production service. I&apos;m the one who answers for it at 3 a.m.&lt;/p&gt;
&lt;h2&gt;At Arupo: Speed Without Lowering the Bar&lt;/h2&gt;
&lt;p&gt;In my own company the constraint is the opposite. There are no corporate restrictions, but there is very little time and a very small team. AI is what lets a small team ship like a bigger one, as long as the quality bar doesn&apos;t drop.&lt;/p&gt;
&lt;p&gt;The best example is the website you&apos;re reading. In a few work sessions it went through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A full redesign with English and Spanish versions.&lt;/li&gt;
&lt;li&gt;An SEO cleanup.&lt;/li&gt;
&lt;li&gt;Security hardening: a Content Security Policy generated at build time, with a check that fails the build if an inline script is missing from it.&lt;/li&gt;
&lt;li&gt;A migration from Astro 5 to Astro 7.&lt;/li&gt;
&lt;li&gt;An accessibility pass to meet WCAG AA contrast, and a dependency audit taken down to zero known vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of those shipped as its own pull request, with preview deploys, before reaching production. The agents did most of the typing. I made every decision that mattered: what to upgrade and what to leave alone, which security trade-off to accept, when something was ready to merge.&lt;/p&gt;
&lt;p&gt;The same setup runs my other projects: finance and fitness side products, a thesis, and the different repositories behind Arupo.&lt;/p&gt;
&lt;h2&gt;The Workflow That Ties It Together&lt;/h2&gt;
&lt;p&gt;Tools are the easy part. What makes this work is the process I follow with them, the same in both worlds:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clarify first.&lt;/strong&gt; If the request is ambiguous, the agent asks before it writes a single line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Size the task.&lt;/strong&gt; Small, well-understood changes are done directly. Large ones get tracked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the spec.&lt;/strong&gt; A large change gets a document: the requirements quoted exactly as I said them, a numbered task list and a log of decisions. Anyone, human or agent, can resume from it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests first.&lt;/strong&gt; When the behavior is testable, the failing test comes before the fix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small commits.&lt;/strong&gt; Reviewable slices, never one giant diff that nobody can really review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review by risk.&lt;/strong&gt; When a change touches security, data, contracts or deployments, an independent reviewer looks at it. When it doesn&apos;t, I don&apos;t pay for ceremony I don&apos;t need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I merge.&lt;/strong&gt; Pushing, opening the pull request and merging remain my decisions. Always.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What Doesn&apos;t Work (Yet)&lt;/h2&gt;
&lt;p&gt;To keep this honest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tools break.&lt;/strong&gt; Some integrations need re-authentication, others fail to connect. A setup this connected needs maintenance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory goes stale.&lt;/strong&gt; A decision saved three months ago may no longer be true. Memory is context, not truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents are confidently wrong.&lt;/strong&gt; They will tell you a build passes when it doesn&apos;t, or invent an API. That&apos;s why every claim needs evidence: the test output, the build log, the diff.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;AI didn&apos;t make me a different engineer. It amplified the one I already was. The fundamentals I learned over eight years, from network infrastructure to leading product teams, are exactly what let me direct these agents and catch them when they&apos;re wrong.&lt;/p&gt;
&lt;p&gt;The skill that matters now isn&apos;t typing fast. It&apos;s knowing what to ask, recognizing when the answer is wrong, and owning the result. That part is still, and will remain, ours.&lt;/p&gt;
</content:encoded></item><item><title>From Monolith to BFF: How We Actually Migrated Without Burning Everything Down</title><link>https://hrdi.dev/blog/monolith-to-bff-clean-architecture/</link><guid isPermaLink="true">https://hrdi.dev/blog/monolith-to-bff-clean-architecture/</guid><description>A real-world account of migrating from a monolithic backend to a BFF pattern with clean architecture — the decisions, the mistakes, and the trade-offs nobody warns you about.</description><pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Let me tell you something that every senior engineer eventually learns: nobody wakes up one morning and decides to migrate away from a monolith for fun. You do it because the monolith is actively hurting your team, your deployments, and your customers. That&apos;s exactly where we were about two years ago, and I want to walk you through how we actually pulled it off — not the sanitized conference talk version, but the real one.&lt;/p&gt;
&lt;h2&gt;The Monolith Was Screaming for Help&lt;/h2&gt;
&lt;p&gt;Our monolith was a Node.js Express application that had been growing for three years. It started clean. It didn&apos;t stay that way. Here&apos;s what the symptoms looked like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deploy fear&lt;/strong&gt;: A change to the payment module required deploying the entire application. One Friday afternoon, a CSS fix in the admin panel brought down the checkout flow. That was a fun weekend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tangled dependencies&lt;/strong&gt;: The order service directly imported the user repository. The notification module knew about inventory levels. Everything knew about everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team bottlenecks&lt;/strong&gt;: Three squads working on the same codebase meant constant merge conflicts and a deploy queue that sometimes backed up for days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance ceiling&lt;/strong&gt;: We couldn&apos;t scale the reporting module independently from the real-time checkout flow. Both lived in the same process, competing for the same resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The monolith wasn&apos;t just technical debt — it was organizational debt. And that&apos;s when you know it&apos;s time.&lt;/p&gt;
&lt;h2&gt;Why BFF (And Not Just &quot;Microservices&quot;)&lt;/h2&gt;
&lt;p&gt;The knee-jerk reaction is &quot;let&apos;s do microservices!&quot; But microservices without a clear frontend strategy just moves the spaghetti from the backend to the API layer. We had three clients: a web SPA (Angular), a mobile app (React Native), and an internal admin dashboard. Each one needed different data shapes, different auth flows, and different performance characteristics.&lt;/p&gt;
&lt;p&gt;That&apos;s where Backend for Frontend comes in. The idea is simple: each client gets its own lightweight backend that orchestrates calls to domain services and shapes the response for that specific client&apos;s needs.&lt;/p&gt;
&lt;p&gt;The key insight was this — &lt;strong&gt;the BFF is not a domain service&lt;/strong&gt;. It has zero business logic. It&apos;s an orchestration and transformation layer. The moment you put business rules in a BFF, you&apos;ve just built another monolith with extra network hops.&lt;/p&gt;
&lt;h2&gt;Hexagonal Architecture in the Domain Services&lt;/h2&gt;
&lt;p&gt;For the actual domain services (orders, inventory, users, payments), we went with hexagonal architecture. Here&apos;s why: we wanted the domain logic to be completely decoupled from infrastructure concerns. Database? That&apos;s an adapter. Message queue? Adapter. External payment gateway? Adapter.&lt;/p&gt;
&lt;p&gt;Here&apos;s the structure we used in NestJS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
├── domain/
│   ├── entities/
│   │   └── order.entity.ts
│   ├── ports/
│   │   ├── order.repository.port.ts
│   │   └── payment-gateway.port.ts
│   └── use-cases/
│       └── create-order.use-case.ts
├── infrastructure/
│   ├── adapters/
│   │   ├── postgres-order.repository.ts
│   │   └── stripe-payment.gateway.ts
│   └── modules/
│       └── order.module.ts
└── application/
    └── bff/
        └── web-bff.controller.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The port definition is clean and infrastructure-agnostic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// domain/ports/order.repository.port.ts
export interface OrderRepositoryPort {
  findById(id: string): Promise&amp;lt;Order | null&amp;gt;;
  save(order: Order): Promise&amp;lt;Order&amp;gt;;
  findByCustomer(customerId: string, pagination: PaginationParams): Promise&amp;lt;PaginatedResult&amp;lt;Order&amp;gt;&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the BFF controller orchestrates without owning logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// application/bff/web-bff.controller.ts
@Controller(&apos;web/orders&apos;)
export class WebBffController {
  constructor(
    private readonly createOrderUseCase: CreateOrderUseCase,
    private readonly getCustomerUseCase: GetCustomerUseCase,
  ) {}

  @Post()
  async createOrder(@Body() dto: CreateOrderDto, @Req() req: AuthRequest) {
    const order = await this.createOrderUseCase.execute({
      customerId: req.user.id,
      items: dto.items,
      shippingAddress: dto.shippingAddress,
    });

    // BFF responsibility: shape response for the web client
    return {
      orderId: order.id,
      status: order.status,
      estimatedDelivery: order.estimatedDelivery?.toISOString(),
      items: order.items.map(item =&amp;gt; ({
        name: item.productName,
        qty: item.quantity,
        total: formatCurrency(item.totalPrice),
      })),
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice: the BFF calls the use case, then transforms the response for the web client. The mobile BFF would return a different shape — maybe with less data to save bandwidth, or with image URLs optimized for mobile screens.&lt;/p&gt;
&lt;h2&gt;The Migration Strategy: Strangler Fig&lt;/h2&gt;
&lt;p&gt;We didn&apos;t do a big bang migration. That&apos;s how you burn everything down. We used the strangler fig pattern, and here&apos;s how it worked in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;API Gateway as the router&lt;/strong&gt;: We put an API Gateway (Kong) in front of everything. Initially, 100% of traffic went to the monolith.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract one domain at a time&lt;/strong&gt;: We started with the notification service because it was the most isolated and lowest risk. We built it as a standalone NestJS service with hexagonal architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dual-write period&lt;/strong&gt;: For about two weeks, both the monolith and the new service processed notifications. We compared outputs. Found three bugs in the new service this way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Route switch&lt;/strong&gt;: Once confident, we updated the gateway to route notification traffic to the new service. The monolith code stayed in place but dormant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeat&lt;/strong&gt;: Orders was next, then inventory, then payments (the scariest one, saved for last).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The whole migration took about eight months. Not eight days, not eight weeks. Eight months.&lt;/p&gt;
&lt;h2&gt;The Mistakes We Made&lt;/h2&gt;
&lt;p&gt;I&apos;d be lying if I said it went smoothly. Here are the mistakes that cost us real time:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistake 1: Shared database too long.&lt;/strong&gt; We initially let the new order service read from the monolith&apos;s database. &quot;We&apos;ll separate it later,&quot; we said. Later took four months, and during that time we had schema coupling that defeated half the purpose of extracting the service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistake 2: BFF doing too much.&lt;/strong&gt; In the first iteration, our web BFF started accumulating business logic. &quot;It&apos;s just a small validation,&quot; someone said. Then another. Within a month, the BFF had 40 lines of business rules that should have lived in the domain service. We had to refactor it back out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistake 3: Not investing in observability early.&lt;/strong&gt; With a monolith, you can follow a request in a single log stream. With distributed services, you need distributed tracing from day one. We added OpenTelemetry in month three. Should have been day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mistake 4: Underestimating data consistency.&lt;/strong&gt; Going from a single database transaction to eventual consistency across services was a mental shift for the whole team. We had to implement the saga pattern for the order-payment-inventory flow, and it took three iterations to get right.&lt;/p&gt;
&lt;h2&gt;The Trade-offs Nobody Tells You About&lt;/h2&gt;
&lt;p&gt;Let me be real about what you&apos;re signing up for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Operational complexity goes up significantly.&lt;/strong&gt; You now have 6 services instead of 1. That&apos;s 6 CI/CD pipelines, 6 sets of logs, 6 health checks, 6 things that can fail independently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local development gets harder.&lt;/strong&gt; Docker Compose became mandatory. Onboarding a new developer went from &quot;clone and run&quot; to a 45-minute setup process (which we eventually automated).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network is now a failure mode.&lt;/strong&gt; Service-to-service calls can timeout, fail, or return stale data. You need retry logic, circuit breakers, and fallback strategies everywhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing changes fundamentally.&lt;/strong&gt; Unit tests are easy. Integration tests across services? That requires contract testing (we used Pact) and a proper staging environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here&apos;s the other side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Teams move independently.&lt;/strong&gt; The payments squad deploys three times a day without coordinating with anyone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling is surgical.&lt;/strong&gt; Black Friday? Scale the order service and payment service. Leave the admin BFF alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure is isolated.&lt;/strong&gt; The notification service went down for 20 minutes last month. Orders kept flowing. In the monolith era, that would have been a full outage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Honest Takeaway&lt;/h2&gt;
&lt;p&gt;If your monolith is working and your team is small, don&apos;t migrate. Seriously. A well-structured monolith beats a poorly implemented distributed system every single day. We migrated because we had real, measurable pain — not because microservices were trendy.&lt;/p&gt;
&lt;p&gt;But if you&apos;re feeling that pain — deploy fear, team bottlenecks, scaling ceilings — then a thoughtful migration using BFF + hexagonal architecture + strangler fig is a battle-tested approach. Just go in with your eyes open about the trade-offs, invest in observability from day one, and for the love of everything good in engineering, keep business logic out of the BFF.&lt;/p&gt;
&lt;p&gt;The best architecture is the one that lets your team ship with confidence. Everything else is details.&lt;/p&gt;
</content:encoded></item><item><title>Why Most Backend Systems Fail at Scale (And the Mistakes I&apos;ve Seen Firsthand)</title><link>https://hrdi.dev/blog/why-backend-systems-fail-at-scale/</link><guid isPermaLink="true">https://hrdi.dev/blog/why-backend-systems-fail-at-scale/</guid><description>Seven real production failures I&apos;ve witnessed — from N+1 queries to cache stampedes — and the fixes that actually worked. No theory, just battle scars.</description><pubDate>Tue, 10 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve spent over six years building and scaling backend systems, and I can tell you this with absolute confidence: most systems don&apos;t fail because of some exotic edge case. They fail because of well-known mistakes that teams keep repeating. Not because engineers are bad — because these problems are invisible at low traffic and only become lethal at scale.&lt;/p&gt;
&lt;p&gt;Here are seven mistakes I&apos;ve seen kill production systems, what actually happened, and how we fixed them. Every single one of these is from a real project.&lt;/p&gt;
&lt;h2&gt;1. The N+1 Query That Nobody Noticed&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; We had an endpoint that returned a list of orders with their associated products. At 50 orders, it was fine — 200ms response time. At 500 orders during a flash sale, it took 14 seconds and the database CPU hit 98%.&lt;/p&gt;
&lt;p&gt;The ORM was doing exactly what we asked: load orders, then for each order, load its products. One query for orders, 500 queries for products. Classic N+1.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; The immediate fix was eager loading — one query with a JOIN. Response time dropped to 180ms regardless of order count.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before: N+1 disaster
const orders = await orderRepo.find({ where: { userId } });
// This triggers 1 query per order to load products

// After: single query with relation loading
const orders = await orderRepo.find({
  where: { userId },
  relations: [&apos;items&apos;, &apos;items.product&apos;],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the real fix was adding query logging in staging with a threshold alert. Any endpoint that executes more than 10 queries per request gets flagged automatically. We catch N+1s before they reach production now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; ORMs make N+1 queries easy to write and hard to spot. You need automated detection, not just code reviews.&lt;/p&gt;
&lt;h2&gt;2. Missing Circuit Breakers&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; Our order service called a third-party shipping rate API. One afternoon, that API started responding with 30-second timeouts instead of its usual 200ms. Every single request to our checkout flow queued up waiting for shipping rates. Thread pool exhausted. Our entire checkout was down — not because OUR system broke, but because someone else&apos;s did.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; We implemented circuit breakers using a pattern similar to Netflix&apos;s Hystrix (we used &lt;code&gt;opossum&lt;/code&gt; in Node.js):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import CircuitBreaker from &apos;opossum&apos;;

const shippingBreaker = new CircuitBreaker(fetchShippingRates, {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000,
});

shippingBreaker.fallback(() =&amp;gt; ({
  rates: getCachedShippingRates(),
  isFallback: true,
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the shipping API fails, the circuit opens, and we serve cached rates with a flag telling the frontend these are estimates. Customers keep checking out. We reconcile actual shipping costs asynchronously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Every external dependency is a liability. If you don&apos;t have a fallback strategy for each one, you&apos;ve coupled your uptime to theirs.&lt;/p&gt;
&lt;h2&gt;3. Shared Database Between Services&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; Two services — orders and inventory — both read and wrote to the same PostgreSQL database. The inventory service ran a heavy analytical query every 5 minutes for stock reports. During peak hours, that query locked rows that the order service needed to update. Orders started failing with lock timeout errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; We gave each service its own database. Inventory changes get published as events via SNS, and the order service maintains its own read model of available stock. Yes, it&apos;s eventually consistent. But &quot;eventually consistent with working checkout&quot; beats &quot;strongly consistent with failing checkout&quot; every time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; A shared database is a shared fate. If two services share a database, they&apos;re not two services — they&apos;re a distributed monolith.&lt;/p&gt;
&lt;h2&gt;4. No Backpressure on Queues&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; We used SQS for processing order confirmations. Each message triggered an email, a PDF invoice generation, and an inventory update. During a promotional event, 50,000 orders came in within an hour. The SQS queue backed up to 200,000 messages (some orders generated multiple messages). Our consumer tried to process all of them at full speed, overwhelmed the email service, and started throwing 429 rate limit errors. Failed messages went to the dead-letter queue. 12,000 customers didn&apos;t get their confirmation emails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; Three changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concurrency limits on consumers:&lt;/strong&gt; Instead of processing as fast as possible, we capped at 20 concurrent messages per consumer instance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting per downstream dependency:&lt;/strong&gt; We added a token bucket rate limiter before calling the email API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exponential backoff with jitter:&lt;/strong&gt; Failed messages get retried with increasing delays plus randomization to avoid thundering herd on recovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// SQS consumer with controlled concurrency
const consumer = Consumer.create({
  queueUrl: process.env.ORDER_QUEUE_URL,
  batchSize: 10,
  visibilityTimeout: 60,
  handleMessage: async (message) =&amp;gt; {
    await rateLimiter.acquire(&apos;email-service&apos;, { maxPerSecond: 50 });
    await processOrderConfirmation(JSON.parse(message.Body));
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; A queue without backpressure is just a way to convert a traffic spike into a cascading failure with extra steps.&lt;/p&gt;
&lt;h2&gt;5. Ignoring Cold Starts&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; We moved a critical service to AWS Lambda for cost savings. Average response time: 80ms. But after periods of inactivity, the first request took 4-6 seconds due to cold starts. This was the authentication service. Users experienced random 5-second login delays. Support tickets piled up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; We evaluated three options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Provisioned concurrency:&lt;/strong&gt; Keeps Lambda instances warm. Effective but expensive — defeated the cost savings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warm-up pings:&lt;/strong&gt; A CloudWatch scheduled event hitting the function every 5 minutes. Cheap but unreliable under concurrent load — only keeps one instance warm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move back to ECS Fargate:&lt;/strong&gt; What we actually did. The auth service needs consistent sub-100ms latency. Lambda&apos;s model doesn&apos;t fit that requirement.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Serverless is fantastic — for the right workloads. Latency-sensitive, always-on services aren&apos;t it. Match the compute model to the actual requirements, not the trend.&lt;/p&gt;
&lt;h2&gt;6. Cache Stampede&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; We cached our product catalog in Redis with a 5-minute TTL. At the exact moment the cache expired, 3,000 concurrent requests hit the endpoint. All 3,000 saw a cache miss. All 3,000 queried the database simultaneously. Database connection pool exhausted. 500 errors for everyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; We implemented a cache lock pattern (sometimes called &quot;probabilistic early expiration&quot;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getProductCatalog(categoryId: string): Promise&amp;lt;Product[]&amp;gt; {
  const cacheKey = `catalog:${categoryId}`;
  const cached = await redis.get(cacheKey);

  if (cached) {
    const { data, expiry, delta } = JSON.parse(cached);
    const now = Date.now();

    // Probabilistic early recomputation
    // As we approach expiry, increasingly likely to refresh
    if (now - delta * Math.log(Math.random()) &amp;lt; expiry) {
      return data;
    }
  }

  // Distributed lock: only one process rebuilds
  const lockKey = `lock:${cacheKey}`;
  const acquired = await redis.set(lockKey, &apos;1&apos;, &apos;EX&apos;, 30, &apos;NX&apos;);

  if (!acquired) {
    // Another process is rebuilding; serve stale if available
    if (cached) return JSON.parse(cached).data;
    // No stale data; wait briefly and retry
    await sleep(100);
    return getProductCatalog(categoryId);
  }

  try {
    const products = await productRepo.findByCategory(categoryId);
    const ttl = 300; // 5 minutes
    await redis.set(cacheKey, JSON.stringify({
      data: products,
      expiry: Date.now() + ttl * 1000,
      delta: ttl * 1000 * 0.1,
    }), &apos;EX&apos;, ttl + 60); // Extra buffer for stale serving
    return products;
  } finally {
    await redis.del(lockKey);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only one process rebuilds the cache. Everyone else gets slightly stale data. In an e-commerce catalog, nobody notices 30-second-old product data. Everyone notices a 500 error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Cache expiration is a coordinated event that can create thundering herds. Design for it explicitly.&lt;/p&gt;
&lt;h2&gt;7. Not Designing for Failure&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; This is the meta-mistake that encompasses all the others. We built our first version of the platform assuming everything would work. Happy path everywhere. No retries, no fallbacks, no graceful degradation. The system worked perfectly in development and staging. Production taught us otherwise within the first week.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; We adopted a principle: &lt;strong&gt;every external call must answer three questions before it&apos;s written:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What happens if this call fails?&lt;/li&gt;
&lt;li&gt;What happens if this call is slow (10x normal latency)?&lt;/li&gt;
&lt;li&gt;What happens if this call returns unexpected data?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We codified this into our code review checklist. Pull requests that add external calls without answering these three questions get sent back.&lt;/p&gt;
&lt;p&gt;We also started running game days — intentionally injecting failures in staging using AWS Fault Injection Simulator. You&apos;d be amazed at how many assumptions break when you kill a database replica or add 500ms latency to an internal service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Failure isn&apos;t an edge case. It&apos;s a feature of distributed systems. If you haven&apos;t designed for it, you haven&apos;t designed the system.&lt;/p&gt;
&lt;h2&gt;The Pattern Behind All These Failures&lt;/h2&gt;
&lt;p&gt;Look at all seven mistakes. There&apos;s a common thread: &lt;strong&gt;they&apos;re all invisible at low scale.&lt;/strong&gt; N+1 queries are fast when N is small. Missing circuit breakers don&apos;t matter when dependencies are healthy. Cache stampedes don&apos;t happen with 10 concurrent users.&lt;/p&gt;
&lt;p&gt;This is why load testing and chaos engineering aren&apos;t optional luxuries — they&apos;re how you find these problems before your customers do. We run load tests that simulate 3x our peak traffic before every major release. We run chaos experiments monthly. It&apos;s not paranoia. It&apos;s professionalism.&lt;/p&gt;
&lt;p&gt;The systems that survive at scale aren&apos;t the ones built by the smartest engineers. They&apos;re the ones built by engineers who assumed everything would break and planned accordingly. Build for the failure case first. The happy path takes care of itself.&lt;/p&gt;
</content:encoded></item><item><title>Redis in Production: Caching Strategies That Actually Work (And Some That Don&apos;t)</title><link>https://hrdi.dev/blog/redis-caching-strategies-production/</link><guid isPermaLink="true">https://hrdi.dev/blog/redis-caching-strategies-production/</guid><description>A production-tested guide to Redis caching strategies — cache-aside, write-through, invalidation patterns, TTL design, and the mistakes that cost us real money.</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been running Redis in production on e-commerce platforms for several years now, and I can tell you that most caching tutorials leave out the parts that actually matter. They show you &lt;code&gt;SET key value EX 300&lt;/code&gt; and call it a day. Real production caching is about trade-offs, invalidation strategies, failure modes, and knowing when NOT to cache something.&lt;/p&gt;
&lt;p&gt;This is everything I wish someone had told me before I learned it the hard way.&lt;/p&gt;
&lt;h2&gt;The Three Core Strategies (And When Each One Fits)&lt;/h2&gt;
&lt;h3&gt;Cache-Aside (Lazy Loading)&lt;/h3&gt;
&lt;p&gt;This is the most common pattern, and for good reason. The application checks the cache first. On a miss, it reads from the database, writes to the cache, and returns the data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getProduct(id: string): Promise&amp;lt;Product&amp;gt; {
  const cacheKey = `product:${id}`;

  // 1. Check cache
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // 2. Cache miss — read from DB
  const product = await productRepo.findById(id);
  if (!product) throw new NotFoundException();

  // 3. Populate cache
  await redis.set(cacheKey, JSON.stringify(product), &apos;EX&apos;, 600);

  return product;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;When it works:&lt;/strong&gt; Read-heavy workloads where stale data is acceptable for short periods. Product catalogs, user profiles, configuration data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When it doesn&apos;t:&lt;/strong&gt; Data that changes frequently and must be immediately consistent. If you cache a user&apos;s cart with cache-aside and they add an item, they&apos;ll see the old cart until the TTL expires (unless you actively invalidate — more on that later).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Simple to implement, but every cache miss is a slow request. Cold cache after a deploy or Redis restart means a thundering herd to the database.&lt;/p&gt;
&lt;h3&gt;Write-Through&lt;/h3&gt;
&lt;p&gt;Every write goes to both the cache and the database simultaneously. Reads always hit the cache.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function updateProduct(id: string, data: UpdateProductDto): Promise&amp;lt;Product&amp;gt; {
  // 1. Update database
  const product = await productRepo.update(id, data);

  // 2. Update cache (synchronously — part of the write path)
  const cacheKey = `product:${id}`;
  await redis.set(cacheKey, JSON.stringify(product), &apos;EX&apos;, 600);

  return product;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;When it works:&lt;/strong&gt; Data that&apos;s read frequently and updated occasionally, AND where you need strong read consistency. Session data, feature flags, pricing rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When it doesn&apos;t:&lt;/strong&gt; Write-heavy workloads. You&apos;re paying the latency cost of a Redis write on every single database write, even for data nobody might read.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Cache is always fresh, but writes are slower. And you still need a TTL as a safety net — if the write-through fails silently, the cache diverges forever without one.&lt;/p&gt;
&lt;h3&gt;Write-Behind (Write-Back)&lt;/h3&gt;
&lt;p&gt;Writes go to the cache first, and the cache asynchronously flushes to the database. This is the fastest for writes but the most dangerous.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When it works:&lt;/strong&gt; High-frequency counters, analytics events, real-time metrics — where losing a few data points is acceptable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When it doesn&apos;t:&lt;/strong&gt; Anything transactional. If Redis crashes before flushing to the database, that data is gone. I would never use this for orders, payments, or anything financial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Maximum write performance, but you&apos;re accepting potential data loss. In my experience, this pattern is rarely the right choice for core business data. We use it for view counts and analytics pipelines — that&apos;s about it.&lt;/p&gt;
&lt;h2&gt;TTL Strategies Beyond &quot;Set It to 5 Minutes&quot;&lt;/h2&gt;
&lt;p&gt;The number one TTL anti-pattern I see is slapping the same TTL on everything. Your product catalog and your real-time inventory count have wildly different freshness requirements. Treat them differently.&lt;/p&gt;
&lt;p&gt;Here&apos;s how we think about TTL design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static reference data&lt;/strong&gt; (countries, categories, tax rates): TTL of 24 hours. Invalidate explicitly on change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semi-static data&lt;/strong&gt; (product details, user profiles): TTL of 10-30 minutes. Stale data is annoying but not harmful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Volatile data&lt;/strong&gt; (inventory counts, pricing): TTL of 30-60 seconds. Or don&apos;t cache at all — sometimes the database is the right answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session/auth data&lt;/strong&gt;: TTL matches the session duration. No more, no less.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also use &lt;strong&gt;adaptive TTLs&lt;/strong&gt; based on access patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getWithAdaptiveTTL(key: string, fetcher: () =&amp;gt; Promise&amp;lt;any&amp;gt;): Promise&amp;lt;any&amp;gt; {
  const cached = await redis.get(key);
  if (cached) {
    // Extend TTL on access (most-recently-used stays warm)
    const currentTTL = await redis.ttl(key);
    if (currentTTL &amp;lt; 120) {
      await redis.expire(key, 600); // Reset to 10 min on access
    }
    return JSON.parse(cached);
  }

  const data = await fetcher();
  await redis.set(key, JSON.stringify(data), &apos;EX&apos;, 600);
  return data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frequently accessed data stays warm. Rarely accessed data naturally evicts. Your cache memory stays focused on what matters.&lt;/p&gt;
&lt;h2&gt;Cache Invalidation: The Real Hard Problem&lt;/h2&gt;
&lt;p&gt;Phil Karlton said there are two hard things in computer science: cache invalidation and naming things. He wasn&apos;t wrong about the first one.&lt;/p&gt;
&lt;p&gt;Here are the invalidation patterns we use, from simplest to most robust:&lt;/p&gt;
&lt;h3&gt;TTL-Based Expiration (Passive)&lt;/h3&gt;
&lt;p&gt;Let it expire naturally. This is fine when stale data is acceptable. It&apos;s simple, and simple is underrated.&lt;/p&gt;
&lt;h3&gt;Explicit Invalidation (Active)&lt;/h3&gt;
&lt;p&gt;When data changes, delete the cache key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function updateProduct(id: string, data: UpdateProductDto): Promise&amp;lt;Product&amp;gt; {
  const product = await productRepo.update(id, data);

  // Delete, don&apos;t update — let the next read repopulate
  await redis.del(`product:${id}`);

  // Don&apos;t forget related caches!
  await redis.del(`catalog:${product.categoryId}`);
  await redis.del(`search:products:*`); // Pattern deletion for search caches

  return product;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Delete, don&apos;t update. If you update the cache and the database write fails or rolls back, your cache has phantom data. Delete is idempotent and safe.&lt;/p&gt;
&lt;p&gt;But notice the &lt;code&gt;search:products:*&lt;/code&gt; — that&apos;s where invalidation gets ugly. A product change might affect category listings, search results, recommendation feeds, and homepage features. Missing even one creates a stale data bug that&apos;s incredibly hard to track down.&lt;/p&gt;
&lt;h3&gt;Event-Driven Invalidation&lt;/h3&gt;
&lt;p&gt;This is what we settled on for anything non-trivial. Database changes publish events, and a dedicated cache invalidation consumer handles the cleanup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Event handler — separate service/consumer
@OnEvent(&apos;product.updated&apos;)
async handleProductUpdate(event: ProductUpdatedEvent) {
  const { productId, categoryId, previousCategoryId } = event;

  const keysToInvalidate = [
    `product:${productId}`,
    `catalog:${categoryId}`,
    `homepage:featured`,
  ];

  // If category changed, invalidate old category too
  if (previousCategoryId &amp;amp;&amp;amp; previousCategoryId !== categoryId) {
    keysToInvalidate.push(`catalog:${previousCategoryId}`);
  }

  await Promise.all(keysToInvalidate.map(key =&amp;gt; redis.del(key)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This centralizes invalidation logic. When a new cache is added, you add its invalidation rule to the event handler — one place to maintain instead of scattered &lt;code&gt;redis.del()&lt;/code&gt; calls across the codebase.&lt;/p&gt;
&lt;h2&gt;Redis Cluster on AWS ElastiCache: What We Learned&lt;/h2&gt;
&lt;p&gt;Running Redis on ElastiCache in cluster mode has some sharp edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key distribution matters.&lt;/strong&gt; Redis Cluster uses hash slots. If you use &lt;code&gt;MGET&lt;/code&gt; across keys that land on different slots, it fails. Use hash tags (&lt;code&gt;{product}:123&lt;/code&gt;, &lt;code&gt;{product}:456&lt;/code&gt;) to colocate related keys on the same shard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failover isn&apos;t instant.&lt;/strong&gt; When a primary node fails, ElastiCache promotes a replica. This takes 10-30 seconds. Your application needs to handle connection errors gracefully during failover. We use &lt;code&gt;ioredis&lt;/code&gt; with &lt;code&gt;retryStrategy&lt;/code&gt; and &lt;code&gt;reconnectOnError&lt;/code&gt; configured properly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory management is critical.&lt;/strong&gt; Set &lt;code&gt;maxmemory-policy&lt;/code&gt; to &lt;code&gt;allkeys-lru&lt;/code&gt; for caching workloads. We&apos;ve seen teams leave it at &lt;code&gt;noeviction&lt;/code&gt; (the default) and then wonder why Redis starts returning errors when it fills up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor replication lag.&lt;/strong&gt; If your read replicas fall behind, reads from replicas serve stale data. We alert at 1 second of replication lag.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When NOT to Cache&lt;/h2&gt;
&lt;p&gt;This is the section most articles skip. Here&apos;s when caching makes things worse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Write-heavy, read-light data.&lt;/strong&gt; If you write 10x more than you read, you&apos;re paying the cache write cost without the read benefit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Highly personalized data with low reuse.&lt;/strong&gt; Caching a unique recommendation feed per user sounds good until you realize your cache hit rate is 3%. You&apos;re using Redis as an expensive, volatile second database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data that must be real-time consistent.&lt;/strong&gt; Account balances, inventory for limited-edition drops, auction bids. The cost of stale data exceeds the cost of a database query.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large objects that rarely repeat.&lt;/strong&gt; Caching a 2MB report PDF in Redis is a waste of memory. Put it in S3.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; If your cache hit rate is below 80%, investigate whether that cache is earning its keep. Below 50%, it&apos;s probably hurting more than helping.&lt;/p&gt;
&lt;h2&gt;Monitoring Cache Health&lt;/h2&gt;
&lt;p&gt;A cache you don&apos;t monitor is a cache waiting to betray you. Here&apos;s what we track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hit rate&lt;/strong&gt; (target: &amp;gt;90%): &lt;code&gt;cache_hits / (cache_hits + cache_misses)&lt;/code&gt;. We expose this as a Prometheus metric and alert if it drops below 80%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency percentiles&lt;/strong&gt;: p50, p95, p99 for Redis operations. If p99 spikes, you might have a hot key or a network issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory usage&lt;/strong&gt;: Percentage of available memory used. Alert at 80%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eviction rate&lt;/strong&gt;: If keys are being evicted faster than expected, your cache is undersized for the workload.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection count&lt;/strong&gt;: Suddenly high connection counts can indicate a connection leak in your application.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We use OpenTelemetry to instrument all Redis calls and ship traces to our observability stack. Every cache operation gets a span with the key pattern (not the full key — you don&apos;t want high-cardinality labels), the operation type, and whether it was a hit or miss.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Simplified OTEL instrumentation wrapper
async function cachedGet&amp;lt;T&amp;gt;(key: string, fetcher: () =&amp;gt; Promise&amp;lt;T&amp;gt;, ttl: number): Promise&amp;lt;T&amp;gt; {
  const span = tracer.startSpan(&apos;cache.get&apos;, {
    attributes: { &apos;cache.key_pattern&apos;: key.split(&apos;:&apos;)[0], &apos;cache.ttl&apos;: ttl },
  });

  try {
    const cached = await redis.get(key);
    if (cached) {
      span.setAttribute(&apos;cache.hit&apos;, true);
      cacheHitCounter.inc({ pattern: key.split(&apos;:&apos;)[0] });
      return JSON.parse(cached);
    }

    span.setAttribute(&apos;cache.hit&apos;, false);
    cacheMissCounter.inc({ pattern: key.split(&apos;:&apos;)[0] });
    const data = await fetcher();
    await redis.set(key, JSON.stringify(data), &apos;EX&apos;, ttl);
    return data;
  } finally {
    span.end();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Bottom Line&lt;/h2&gt;
&lt;p&gt;Redis is one of the most powerful tools in a backend engineer&apos;s toolkit, but it&apos;s not magic. It&apos;s a trade-off machine: memory for speed, consistency for performance, simplicity for resilience. Every caching decision should start with &quot;what&apos;s the cost of stale data?&quot; and &quot;what&apos;s the cost of a cache miss?&quot; — not with &quot;let&apos;s cache everything and hope for the best.&quot;&lt;/p&gt;
&lt;p&gt;Start with cache-aside for most things. Add write-through only where consistency matters. Use event-driven invalidation for anything complex. Monitor your hit rates religiously. And never, ever forget: the fastest cache miss is the one you prevent by not caching data that shouldn&apos;t be cached in the first place.&lt;/p&gt;
</content:encoded></item><item><title>Designing Systems When You Don&apos;t Control All the Dependencies</title><link>https://hrdi.dev/blog/designing-systems-without-control/</link><guid isPermaLink="true">https://hrdi.dev/blog/designing-systems-without-control/</guid><description>Building resilient systems when third-party APIs, legacy services, and external teams are unreliable. Circuit breakers, bulkheads, fallbacks, and the trust boundary concept from real e-commerce experience.</description><pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There&apos;s a moment in every engineer&apos;s career where you realize something uncomfortable: the system you&apos;re responsible for is only as reliable as its weakest external dependency. And you don&apos;t control that dependency.&lt;/p&gt;
&lt;p&gt;Maybe it&apos;s a payment gateway that times out during peak hours. Maybe it&apos;s a legacy inventory system maintained by a team in another timezone that deploys on Fridays. Maybe it&apos;s a shipping provider whose API returns 500s every time they push a &quot;minor update.&quot;&lt;/p&gt;
&lt;p&gt;I&apos;ve lived through all of these. And here&apos;s what I&apos;ve learned: &lt;strong&gt;you don&apos;t design resilient systems by hoping everything works. You design them by assuming everything will break.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Trust Boundary Concept&lt;/h2&gt;
&lt;p&gt;Before diving into patterns, let&apos;s establish a mental model I use constantly: the &lt;strong&gt;trust boundary&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Every system has a boundary between what you control and what you don&apos;t. Inside the boundary, you own the code, the deployments, the monitoring, and the on-call rotation. Outside the boundary, you&apos;re at the mercy of someone else&apos;s priorities, release schedule, and definition of &quot;reliable.&quot;&lt;/p&gt;
&lt;p&gt;The mistake most teams make is treating external dependencies as if they&apos;re inside the trust boundary. They call a third-party API and assume it&apos;ll respond in 200ms. They integrate with an internal legacy service and assume the contract won&apos;t change. They trust.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don&apos;t trust. Verify, isolate, and plan for failure.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s how I draw trust boundaries in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tier 1 (full trust):&lt;/strong&gt; Services my team owns and deploys. We control SLAs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tier 2 (conditional trust):&lt;/strong&gt; Internal services from other teams. We have some influence but no control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tier 3 (zero trust):&lt;/strong&gt; Third-party APIs, legacy systems, anything behind a VPN we can&apos;t monitor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each tier gets a different resilience strategy. Tier 3 dependencies get the full treatment.&lt;/p&gt;
&lt;h2&gt;Circuit Breaker Pattern: Stop Hammering Dead Services&lt;/h2&gt;
&lt;p&gt;The circuit breaker is the first pattern you should reach for when dealing with unreliable dependencies. The concept is borrowed from electrical engineering: when a downstream service is failing, stop sending requests to it.&lt;/p&gt;
&lt;p&gt;Here&apos;s a simplified implementation I&apos;ve used in Node.js services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CircuitBreaker {
  private failures = 0;
  private lastFailure: number = 0;
  private state: &apos;closed&apos; | &apos;open&apos; | &apos;half-open&apos; = &apos;closed&apos;;

  constructor(
    private readonly threshold: number = 5,
    private readonly resetTimeout: number = 30_000
  ) {}

  async execute&amp;lt;T&amp;gt;(fn: () =&amp;gt; Promise&amp;lt;T&amp;gt;, fallback: () =&amp;gt; T): Promise&amp;lt;T&amp;gt; {
    if (this.state === &apos;open&apos;) {
      if (Date.now() - this.lastFailure &amp;gt; this.resetTimeout) {
        this.state = &apos;half-open&apos;;
      } else {
        return fallback();
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      return fallback();
    }
  }

  private onSuccess(): void {
    this.failures = 0;
    this.state = &apos;closed&apos;;
  }

  private onFailure(): void {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures &amp;gt;= this.threshold) {
      this.state = &apos;open&apos;;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In an e-commerce system I worked on, we wrapped every payment gateway call with a circuit breaker. When Stripe had intermittent issues (yes, even Stripe has bad days), the circuit would open after 5 consecutive failures. Instead of queuing thousands of failed payment attempts and making users stare at a spinner, we&apos;d immediately route to the fallback flow: show a &quot;payment processing delayed&quot; message and queue the charge for retry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The circuit breaker doesn&apos;t fix the problem. It prevents cascade failures and gives users a better experience during outages.&lt;/p&gt;
&lt;h2&gt;Bulkhead Isolation: Don&apos;t Let One Failure Sink the Ship&lt;/h2&gt;
&lt;p&gt;Bulkheads in ships are watertight compartments. If one compartment floods, the ship doesn&apos;t sink. The same principle applies to software.&lt;/p&gt;
&lt;p&gt;In a real scenario: we had an e-commerce platform where the shipping rate calculator, the inventory checker, and the payment processor all shared the same HTTP connection pool. When the shipping provider started responding slowly, it consumed all available connections. Suddenly, payments couldn&apos;t process either. One bad dependency took down an unrelated critical path.&lt;/p&gt;
&lt;p&gt;The fix was bulkhead isolation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Separate connection pools&lt;/strong&gt; per external dependency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate thread pools&lt;/strong&gt; (or in Node.js, separate queue workers) for different categories of work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate timeouts&lt;/strong&gt; tuned to each dependency&apos;s expected behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const paymentClient = new HttpClient({
  maxConnections: 20,
  timeout: 5_000,       // payments should be fast
  retries: 2,
});

const shippingClient = new HttpClient({
  maxConnections: 10,
  timeout: 15_000,      // shipping APIs are notoriously slow
  retries: 3,
});

const inventoryClient = new HttpClient({
  maxConnections: 15,
  timeout: 3_000,       // internal service, should be quick
  retries: 1,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not over-engineering. This is the difference between &quot;the shipping API is slow today&quot; and &quot;the entire checkout is down.&quot;&lt;/p&gt;
&lt;h2&gt;Timeout Strategies: The Silent Killer&lt;/h2&gt;
&lt;p&gt;Most production incidents I&apos;ve debugged didn&apos;t start with a crash. They started with a timeout that was either too generous or nonexistent.&lt;/p&gt;
&lt;p&gt;Here&apos;s my timeout hierarchy for external calls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Connect timeout:&lt;/strong&gt; How long to wait for a TCP connection. Keep this aggressive (1-3 seconds). If a service isn&apos;t accepting connections, waiting longer won&apos;t help.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read timeout:&lt;/strong&gt; How long to wait for a response after the connection is established. This depends on the operation, but I rarely go above 10 seconds for synchronous calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total timeout:&lt;/strong&gt; The overall budget for the entire operation, including retries. This is the one most people forget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The rule I follow:&lt;/strong&gt; every HTTP call to an external service MUST have an explicit timeout. No exceptions. Default timeouts in HTTP libraries are usually 30-120 seconds, which is an eternity in a request pipeline.&lt;/p&gt;
&lt;p&gt;A practical example: in a checkout flow, the total budget is maybe 8 seconds before the user gets impatient. If you&apos;re calling a payment API, an address validator, and a tax calculator in sequence, you don&apos;t have 30 seconds per call. You have maybe 2-3 seconds each, with the payment getting the most generous allocation.&lt;/p&gt;
&lt;h2&gt;Fallback Mechanisms: Graceful Degradation in Practice&lt;/h2&gt;
&lt;p&gt;Here&apos;s where it gets interesting. Fallbacks aren&apos;t just about returning cached data. They&apos;re about &lt;strong&gt;deciding what your system can still do without a dependency.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Real examples from systems I&apos;ve built:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Payment gateway down:&lt;/strong&gt; Queue the order, show &quot;processing&quot; status, charge asynchronously when the gateway recovers. The user still completes checkout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shipping rate calculator down:&lt;/strong&gt; Show estimated flat rates based on historical data. Not perfect, but the user can still buy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inventory service down:&lt;/strong&gt; Allow the purchase but flag it for manual review. Accept the risk of overselling a few items vs. blocking all sales.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommendation engine down:&lt;/strong&gt; Show popular products instead of personalized ones. Nobody notices.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function getShippingRates(order: Order): Promise&amp;lt;ShippingRate[]&amp;gt; {
  try {
    return await shippingCircuitBreaker.execute(
      () =&amp;gt; shippingClient.calculateRates(order),
      () =&amp;gt; getCachedFlatRates(order.destination)
    );
  } catch {
    return getDefaultFlatRates();  // last resort
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The business conversation matters here.&lt;/strong&gt; Fallbacks have trade-offs. Showing flat rates when the real rates are different means you might lose money on some shipments. Queue-and-charge means you might charge a card that gets declined later. These are business decisions, not just technical ones. Have this conversation with product before the outage, not during.&lt;/p&gt;
&lt;h2&gt;Contract Testing: Catch Breaks Before Production&lt;/h2&gt;
&lt;p&gt;One of the most effective strategies I&apos;ve adopted is consumer-driven contract testing. Instead of praying that the shipping API doesn&apos;t change their response format, we codify what we expect and test it regularly.&lt;/p&gt;
&lt;p&gt;Tools like Pact let you define contracts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe(&apos;Shipping API Contract&apos;, () =&amp;gt; {
  it(&apos;should return rates with expected structure&apos;, async () =&amp;gt; {
    const response = await provider.executeTest(async (mockServer) =&amp;gt; {
      const client = new ShippingClient(mockServer.url);
      const rates = await client.getRates(testOrder);

      expect(rates).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            carrier: expect.any(String),
            price: expect.any(Number),
            estimatedDays: expect.any(Number),
          }),
        ])
      );
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This won&apos;t prevent the external API from breaking. But it&apos;ll tell you &lt;strong&gt;immediately&lt;/strong&gt; when the contract is violated, instead of discovering it through production errors at 2 AM.&lt;/p&gt;
&lt;h2&gt;The Trade-Off: Resilience vs. Simplicity&lt;/h2&gt;
&lt;p&gt;Here&apos;s the thing I want to be honest about: all of this adds complexity. Circuit breakers, bulkheads, fallbacks, contract tests — they all add code, configuration, and cognitive overhead.&lt;/p&gt;
&lt;p&gt;Not every system needs all of these patterns. A side project calling one external API? Just add a timeout and handle errors gracefully. An e-commerce platform processing thousands of orders per hour across a dozen external services? You need the full toolkit.&lt;/p&gt;
&lt;p&gt;My rule of thumb:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If downtime of the dependency costs money:&lt;/strong&gt; circuit breaker + fallback + monitoring&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If downtime causes data loss:&lt;/strong&gt; bulkhead isolation + retries with idempotency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If the dependency is owned by another team:&lt;/strong&gt; contract tests + explicit SLAs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If it&apos;s a third-party you can&apos;t influence at all:&lt;/strong&gt; all of the above&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;The hardest lesson in system design isn&apos;t learning patterns. It&apos;s accepting that you can&apos;t control everything. Your payment provider will have outages. The legacy system will change without warning. The team across the building will deploy a breaking change on a Friday.&lt;/p&gt;
&lt;p&gt;Your job isn&apos;t to prevent these failures. It&apos;s to design systems that handle them with grace. Every resilience pattern is a bet: &quot;When this dependency fails — and it will — here&apos;s how we&apos;ll keep going.&quot;&lt;/p&gt;
&lt;p&gt;The engineers who build truly reliable systems aren&apos;t the ones who write the most clever code. They&apos;re the ones who&apos;ve been burned enough times to plan for the fire.&lt;/p&gt;
</content:encoded></item><item><title>The Role of a Tech Lead Beyond Writing Code</title><link>https://hrdi.dev/blog/tech-lead-beyond-coding/</link><guid isPermaLink="true">https://hrdi.dev/blog/tech-lead-beyond-coding/</guid><description>What being a Tech Lead actually means beyond coding. Architectural decisions, mentoring engineers, navigating product tensions, and the transition from best coder to best enabler.</description><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I first became a Tech Lead, I thought the job was simple: be the best coder on the team and make the big technical decisions. I was wrong about pretty much everything.&lt;/p&gt;
&lt;p&gt;The role of a Tech Lead is one of the most misunderstood positions in software engineering. It&apos;s not a promotion for writing great code. It&apos;s a fundamentally different job. And nobody prepares you for it.&lt;/p&gt;
&lt;p&gt;After years of leading teams across multiple production systems, here&apos;s what the role actually looks like — the parts nobody talks about in job descriptions.&lt;/p&gt;
&lt;h2&gt;Architectural Decisions Are Organizational Decisions&lt;/h2&gt;
&lt;p&gt;Every architectural choice you make has organizational consequences. Choose microservices? Now you need team boundaries, API contracts, and deployment coordination. Choose a monolith? Now you need modular discipline and merge conflict resolution strategies.&lt;/p&gt;
&lt;p&gt;I&apos;ve seen teams adopt event-driven architectures because it was &quot;the right pattern&quot; without considering that nobody on the team had experience with eventual consistency. The architecture was sound on paper. In practice, it created months of debugging sessions and a backlog of data inconsistency bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;As a Tech Lead, your job isn&apos;t to pick the theoretically best architecture. It&apos;s to pick the best architecture for your team, your timeline, and your business constraints.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means asking questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can our team maintain this in 6 months when I&apos;m not reviewing every PR?&lt;/li&gt;
&lt;li&gt;Does this architecture match our deployment capabilities?&lt;/li&gt;
&lt;li&gt;If we hire two junior devs next quarter, will they be able to contribute to this?&lt;/li&gt;
&lt;li&gt;What&apos;s the cost of being wrong about this choice?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best architectural decision I ever made wasn&apos;t choosing an elegant pattern. It was choosing a boring, well-understood one that the entire team could work with independently. Boring is underrated.&lt;/p&gt;
&lt;h2&gt;Mentoring: Your Multiplier Effect&lt;/h2&gt;
&lt;p&gt;Here&apos;s a math problem: you can write 8 hours of code per day, or you can spend 2 hours mentoring three engineers who then each write 6 effective hours of code per day. The first option produces 8 hours of output. The second produces 18.&lt;/p&gt;
&lt;p&gt;This is the multiplier effect, and it&apos;s the single most impactful thing a Tech Lead does.&lt;/p&gt;
&lt;p&gt;But mentoring isn&apos;t just answering questions or doing pair programming (though both matter). Real mentoring means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code review as teaching:&lt;/strong&gt; Don&apos;t just approve or request changes. Explain &lt;em&gt;why&lt;/em&gt; something should be different. Link to documentation. Show the consequences of the current approach. A PR comment that says &quot;use dependency injection here&quot; is worthless. A comment that says &quot;if we inject this dependency, we can test the payment flow without hitting Stripe — here&apos;s how&quot; teaches a principle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delegating the hard problems:&lt;/strong&gt; This is counterintuitive. Your instinct is to take the complex tasks because you&apos;ll do them faster. Resist that instinct. Give the challenging work to growing engineers with your support. Yes, it&apos;ll take longer. Yes, the first implementation won&apos;t be as clean. But now you have two people who can solve that kind of problem instead of one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creating safe failure spaces:&lt;/strong&gt; Engineers who are afraid to make mistakes don&apos;t grow. I explicitly tell my team: &quot;This is a safe area to experiment. If it breaks, we&apos;ll fix it together.&quot; The best engineers I&apos;ve mentored made spectacular mistakes early because they felt safe to try.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of my proudest moments as a Tech Lead wasn&apos;t shipping a feature. It was watching a mid-level engineer I&apos;d been mentoring for a year lead an architectural discussion and make better decisions than I would have.&lt;/p&gt;
&lt;h2&gt;Navigating Product vs. Engineering Tensions&lt;/h2&gt;
&lt;p&gt;Every sprint planning is a negotiation between what the product team wants and what the engineering team knows is sustainable. As Tech Lead, you&apos;re the translator between these two worlds.&lt;/p&gt;
&lt;p&gt;Product says: &quot;We need this feature by next sprint.&quot; Engineering knows: the data model doesn&apos;t support it, and bolting it on will create debt that slows everything down for months.&lt;/p&gt;
&lt;p&gt;Your job isn&apos;t to say &quot;no.&quot; Your job is to say &quot;here are the options with their trade-offs.&quot;&lt;/p&gt;
&lt;p&gt;A framework I use constantly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Do it the fast way. Ship in 1 sprint. Creates X amount of tech debt. Estimated cost to fix later: Y sprints. Risk: Z.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Do it properly. Ship in 2 sprints. No debt. Enables future features P and Q without rework.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option C:&lt;/strong&gt; Ship a reduced scope in 1 sprint. Addresses 80% of user need. Proper architecture. Iterate on remaining 20% in the next sprint.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nine times out of ten, option C wins. But you can only present option C if you deeply understand both the business goal and the technical landscape.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Tech Leads who get burned out are the ones who just say &quot;yes&quot; to everything.&lt;/strong&gt; Learn to present options. Let the stakeholders decide with full information. That&apos;s not resistance — that&apos;s partnership.&lt;/p&gt;
&lt;h2&gt;Saying &quot;No&quot; to Scope Creep&lt;/h2&gt;
&lt;p&gt;Scope creep is the silent project killer. It doesn&apos;t arrive as one big change. It arrives as a series of &quot;small&quot; additions that seem reasonable in isolation.&lt;/p&gt;
&lt;p&gt;&quot;Can we also add filtering to this page?&quot; &quot;What if we supported CSV export too?&quot; &quot;Could this API also handle the admin use case?&quot;&lt;/p&gt;
&lt;p&gt;Each request is 2-3 days of work. Ten of them is a month of unplanned work that nobody budgeted for.&lt;/p&gt;
&lt;p&gt;As a Tech Lead, you&apos;re often the only person in the room who can see the cumulative technical cost. It&apos;s your responsibility to make it visible.&lt;/p&gt;
&lt;p&gt;I keep a running document I call the &quot;scope ledger.&quot; Every addition gets logged with its estimated effort. When stakeholders ask why the project is delayed, I can point to 15 &quot;small&quot; additions that added up to 6 weeks of extra work. Data changes conversations.&lt;/p&gt;
&lt;h2&gt;Technical Debt as a Business Conversation&lt;/h2&gt;
&lt;p&gt;Here&apos;s something that took me years to learn: engineers talk about technical debt in engineering terms. &quot;The codebase is messy.&quot; &quot;We need to refactor this module.&quot; &quot;Our test coverage is too low.&quot; These statements mean nothing to business stakeholders.&lt;/p&gt;
&lt;p&gt;Technical debt needs to be translated into business impact:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Don&apos;t say: &quot;We need to refactor the payment module.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Say: &quot;The payment module takes 3 days to add any new payment method. Our competitors are adding new methods quarterly. Refactoring reduces that to half a day.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Don&apos;t say: &quot;Our test coverage is 20%.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Say: &quot;We&apos;ve had 4 production incidents this quarter that would have been caught by tests. Each incident cost us an average of $X in lost transactions and Y hours of engineering time.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Don&apos;t say: &quot;The codebase is tightly coupled.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Say: &quot;Adding the feature you want requires modifying 12 files across 4 services because of how things are connected. This is why estimates keep being higher than expected.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you frame debt as a business problem with measurable costs, you get budget to fix it. When you frame it as an engineering preference, you get told to &quot;make it work anyway.&quot;&lt;/p&gt;
&lt;h2&gt;The Loneliness of Leading&lt;/h2&gt;
&lt;p&gt;Nobody warns you about this part. As a Tech Lead, you occupy a strange middle ground. You&apos;re not management, but you&apos;re not &quot;one of the developers&quot; anymore either. You&apos;re in meetings your team isn&apos;t in. You know about reorgs, budget cuts, and strategic pivots before anyone else. You can&apos;t always share what you know.&lt;/p&gt;
&lt;p&gt;Your team will sometimes be frustrated with decisions you&apos;ve made or agreed to. They won&apos;t always have the context for why. And you can&apos;t always give them that context.&lt;/p&gt;
&lt;p&gt;There are days where you&apos;re the shield between your team and organizational chaos, and nobody will ever know. The best compliment a Tech Lead can receive isn&apos;t &quot;great architecture.&quot; It&apos;s a team member saying &quot;I don&apos;t know why, but things just work smoothly here.&quot; That smoothness is you absorbing friction they never see.&lt;/p&gt;
&lt;p&gt;Find a peer group. Talk to other Tech Leads. This role is isolating if you don&apos;t have people who understand the specific pressures you&apos;re under.&lt;/p&gt;
&lt;h2&gt;When to Code and When to Step Back&lt;/h2&gt;
&lt;p&gt;This is the hardest calibration. Code too much, and you become a bottleneck — the team depends on you for critical path work. Code too little, and you lose touch with the codebase, making your architectural guidance increasingly theoretical.&lt;/p&gt;
&lt;p&gt;My current balance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I write code&lt;/strong&gt; for: proof of concepts for architectural decisions, critical infrastructure pieces where the patterns need to be established, debugging production issues that require deep system knowledge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I don&apos;t write code&lt;/strong&gt; for: feature work that a team member could do (even if slower), anything on the critical path of a sprint commitment, tasks that would be better as learning opportunities for someone else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical rule: if removing me from the sprint board would block delivery, I&apos;m coding too much of the wrong things. The team should be able to ship without me writing a single line of feature code.&lt;/p&gt;
&lt;h2&gt;The Transition: Best Coder to Best Enabler&lt;/h2&gt;
&lt;p&gt;The most difficult shift in becoming a Tech Lead is redefining what &quot;productive&quot; means. For your entire career, productivity meant lines of code, features shipped, problems solved with your own hands.&lt;/p&gt;
&lt;p&gt;Now, productivity means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A junior engineer who ships their first production feature with confidence&lt;/li&gt;
&lt;li&gt;An architecture that enables the team to move independently&lt;/li&gt;
&lt;li&gt;A code review culture where engineers teach each other&lt;/li&gt;
&lt;li&gt;A sprint that finishes on time because you identified risks early&lt;/li&gt;
&lt;li&gt;A production system that handles failure gracefully because you invested in resilience patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your code output will drop. Your impact will multiply. That&apos;s not a trade-off. That&apos;s the entire point.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Being a Tech Lead changed how I think about software. I used to optimize for elegant code. Now I optimize for team velocity, sustainable systems, and people growth.&lt;/p&gt;
&lt;p&gt;If you&apos;re transitioning into this role, here&apos;s my honest advice: let go of being the best coder in the room. Invest in being the person who makes everyone else better. The code you write will be forgotten. The engineers you grow will build things you never could have built alone.&lt;/p&gt;
&lt;p&gt;That&apos;s not a demotion. That&apos;s leverage.&lt;/p&gt;
</content:encoded></item><item><title>AI in the Developer Workflow: What Actually Works (And What&apos;s Just Hype)</title><link>https://hrdi.dev/blog/ai-developer-workflow/</link><guid isPermaLink="true">https://hrdi.dev/blog/ai-developer-workflow/</guid><description>An honest assessment of AI tools in daily development. What AI is genuinely good at, what it&apos;s terrible at, and why understanding fundamentals matters more than ever.</description><pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Let me be direct: AI is not going to replace developers. But it&apos;s also not a toy. The truth, as usual, is somewhere in the middle — and most of the discourse around AI in development is either breathless hype or dismissive cynicism. Both are wrong.&lt;/p&gt;
&lt;p&gt;I&apos;ve been using AI tools daily in my development workflow for over a year now. Not as an experiment. In production work, leading teams, shipping real systems. Here&apos;s my honest, no-hype assessment of what actually works, what doesn&apos;t, and what you should be thinking about.&lt;/p&gt;
&lt;h2&gt;What AI Is Genuinely Good At&lt;/h2&gt;
&lt;p&gt;Let&apos;s start with the wins. These are areas where AI tools have measurably improved my workflow:&lt;/p&gt;
&lt;h3&gt;Boilerplate and Repetitive Code&lt;/h3&gt;
&lt;p&gt;This is where AI shines brightest. Writing DTOs, mapping functions, CRUD endpoints, configuration files, type definitions from API specs — the kind of code where the pattern is well-established and you&apos;re essentially translating a known structure into syntax.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// &quot;Generate a DTO for the order response with these fields&quot;
// AI produces this in seconds vs. minutes of typing:
interface OrderResponseDto {
  id: string;
  status: OrderStatus;
  items: OrderItemDto[];
  total: number;
  currency: string;
  createdAt: string;
  shippingAddress: AddressDto;
  estimatedDelivery: string | null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is this revolutionary? No. Does it save 20 minutes of tedious typing multiple times per day? Absolutely. That adds up.&lt;/p&gt;
&lt;h3&gt;Test Scaffolding&lt;/h3&gt;
&lt;p&gt;This is probably my single favorite use case. AI is remarkably good at generating test scaffolds — the describe blocks, the setup, the basic happy-path and error cases. It gets the structure right roughly 80% of the time.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// I describe what the function does, AI generates the scaffold:
describe(&apos;OrderService.calculateTotal&apos;, () =&amp;gt; {
  let service: OrderService;
  let mockTaxCalculator: jest.Mocked&amp;lt;TaxCalculator&amp;gt;;

  beforeEach(() =&amp;gt; {
    mockTaxCalculator = { calculate: jest.fn() };
    service = new OrderService(mockTaxCalculator);
  });

  it(&apos;should sum item prices correctly&apos;, () =&amp;gt; { /* ... */ });
  it(&apos;should apply tax from calculator&apos;, () =&amp;gt; { /* ... */ });
  it(&apos;should handle empty order&apos;, () =&amp;gt; { /* ... */ });
  it(&apos;should throw on negative quantities&apos;, () =&amp;gt; { /* ... */ });
  it(&apos;should round to 2 decimal places&apos;, () =&amp;gt; { /* ... */ });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I still write the actual assertions and edge cases myself. But having the scaffold, the mocks set up, and the test organization ready saves significant time. It&apos;s like having someone set up the workbench before you start building.&lt;/p&gt;
&lt;h3&gt;Exploring Unfamiliar Codebases&lt;/h3&gt;
&lt;p&gt;When I join a new project or need to understand a library I&apos;ve never used, AI is an excellent exploration partner. &quot;Explain how this middleware chain works.&quot; &quot;What does this Terraform module provision?&quot; &quot;Walk me through the authentication flow in this codebase.&quot;&lt;/p&gt;
&lt;p&gt;It&apos;s not always right. But it&apos;s faster than reading documentation from scratch, and it gives me a starting point for my own investigation. I treat it like asking a colleague who&apos;s worked on the codebase before — useful input, but I verify anything critical.&lt;/p&gt;
&lt;h3&gt;Documentation and Commit Messages&lt;/h3&gt;
&lt;p&gt;Writing JSDoc comments, README sections, API documentation, and even thoughtful commit messages. AI handles this surprisingly well because documentation is fundamentally about describing what already exists — and the code is right there as context.&lt;/p&gt;
&lt;h3&gt;Refactoring Suggestions&lt;/h3&gt;
&lt;p&gt;&quot;Here&apos;s a 200-line function. Suggest how to break it down.&quot; The suggestions aren&apos;t always production-ready, but they&apos;re a useful starting point for thinking about decomposition. AI can spot extract-method opportunities faster than I can read through a long function.&lt;/p&gt;
&lt;h2&gt;What AI Is Terrible At&lt;/h2&gt;
&lt;p&gt;Now the important part. These are areas where trusting AI output will hurt you:&lt;/p&gt;
&lt;h3&gt;Architecture Decisions&lt;/h3&gt;
&lt;p&gt;AI cannot make architectural decisions for your system. It doesn&apos;t understand your team&apos;s capabilities, your deployment constraints, your business timeline, your compliance requirements, or the political dynamics that influence technical choices.&lt;/p&gt;
&lt;p&gt;I&apos;ve seen engineers ask AI &quot;should I use microservices or a monolith?&quot; and treat the response as gospel. The response will be coherent. It&apos;ll mention trade-offs. It&apos;ll sound authoritative. And it&apos;ll be generic advice that ignores everything that actually matters about your specific situation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture requires context that doesn&apos;t fit in a prompt.&lt;/strong&gt; It requires understanding organizational dynamics, team skills, operational maturity, and business trajectory. AI has none of this.&lt;/p&gt;
&lt;h3&gt;Business Logic&lt;/h3&gt;
&lt;p&gt;The core logic that makes your system valuable — the pricing algorithms, the matching rules, the compliance checks, the domain-specific calculations — this is exactly where AI is most dangerous.&lt;/p&gt;
&lt;p&gt;Why? Because it&apos;ll generate code that &lt;em&gt;looks&lt;/em&gt; correct. It compiles. It passes basic tests. But it encodes assumptions about your business domain that may be subtly wrong. And subtle bugs in business logic are the kind that cost real money and erode user trust.&lt;/p&gt;
&lt;p&gt;I had a case where AI-generated code for a discount calculation handled percentage-based discounts correctly but applied them in the wrong order when stacked with flat-rate discounts. The code looked clean. The tests that AI also generated passed. The bug made it to staging and would have cost thousands in incorrectly priced orders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your business logic is your competitive advantage. Write it yourself. Understand every line.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;p&gt;Do not let AI write your authentication flows, authorization logic, input validation, or anything related to security. AI models are trained on a massive corpus of code, and a significant portion of that code has security vulnerabilities.&lt;/p&gt;
&lt;p&gt;AI will generate SQL queries without parameterization. It&apos;ll create JWT validation that doesn&apos;t check expiration. It&apos;ll write file upload handlers without size limits or type checking. Not every time — but often enough that you cannot trust it.&lt;/p&gt;
&lt;p&gt;Security is a domain where &quot;works most of the time&quot; is completely unacceptable.&lt;/p&gt;
&lt;h2&gt;The &quot;AI-Assisted&quot; vs. &quot;AI-Generated&quot; Distinction&lt;/h2&gt;
&lt;p&gt;This is the framework I use to think about AI in my workflow, and I think it&apos;s the most important distinction in this entire conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI-Assisted:&lt;/strong&gt; I&apos;m driving. I understand the problem, I know what the solution should look like, and I&apos;m using AI to accelerate the mechanical parts of implementation. I review every line, I make the decisions, AI handles the typing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI-Generated:&lt;/strong&gt; AI is driving. I describe what I want, AI produces the solution, and I ship it with minimal review. I&apos;m essentially outsourcing understanding.&lt;/p&gt;
&lt;p&gt;The first approach makes you faster. The second makes you dangerous.&lt;/p&gt;
&lt;p&gt;Here&apos;s my rule: &lt;strong&gt;if you can&apos;t explain every line of the AI-generated code, you shouldn&apos;t ship it.&lt;/strong&gt; Not &quot;I roughly understand what it does&quot; — you should be able to explain &lt;em&gt;why&lt;/em&gt; each decision was made, what the edge cases are, and how it fails.&lt;/p&gt;
&lt;p&gt;If you can&apos;t do that, you don&apos;t have a solution. You have a liability.&lt;/p&gt;
&lt;h2&gt;How I Actually Use AI Day-to-Day&lt;/h2&gt;
&lt;p&gt;Here&apos;s my realistic workflow:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Morning code review acceleration:&lt;/strong&gt; I use AI to get a quick summary of large PRs before doing my detailed review. &quot;Summarize the changes in this PR and flag potential issues.&quot; This gives me a map before I dive into the territory. I still review every file, but I know where to focus attention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test generation:&lt;/strong&gt; When writing a new service, I write the implementation first, then ask AI to generate test scaffolds. I modify the tests heavily — adding edge cases, fixing assertions, ensuring the tests actually validate behavior rather than implementation. But the scaffold saves 30-40% of the time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exploring new libraries:&lt;/strong&gt; Before reading full documentation, I ask AI to explain the core concepts and common patterns. This gives me a mental model that makes the docs more digestible. Always followed by reading the actual docs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Boilerplate acceleration:&lt;/strong&gt; DTOs, mappers, configuration, migration files. Anything where the pattern is established and I&apos;m translating a known structure into code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I never use AI for:&lt;/strong&gt; Architecture decisions, database schema design, security logic, performance-critical code paths, business rule implementation, or any code where being subtly wrong has significant consequences.&lt;/p&gt;
&lt;h2&gt;Why Fundamentals Matter MORE With AI, Not Less&lt;/h2&gt;
&lt;p&gt;Here&apos;s the take that might be controversial: &lt;strong&gt;AI makes software engineering fundamentals more important, not less.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you don&apos;t understand design patterns, you can&apos;t evaluate whether AI&apos;s suggested pattern is appropriate. If you don&apos;t understand database indexing, you can&apos;t spot that AI&apos;s generated query will do a full table scan on a million-row table. If you don&apos;t understand HTTP semantics, you can&apos;t catch that AI used POST where it should have used PUT.&lt;/p&gt;
&lt;p&gt;AI amplifies what you already know. If you understand system design deeply, AI makes you dramatically faster. If you don&apos;t understand the fundamentals, AI makes you dramatically more efficient at producing broken software.&lt;/p&gt;
&lt;p&gt;The developers who are thriving with AI tools are the ones who already had strong fundamentals. They use AI as a power tool, not a crutch. They know enough to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Evaluate&lt;/strong&gt; AI output critically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Catch&lt;/strong&gt; subtle errors that look correct at first glance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct&lt;/strong&gt; AI toward the right solution by asking better questions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know when NOT to use it&lt;/strong&gt; — which might be the most important skill of all&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Danger of The Comfort Zone&lt;/h2&gt;
&lt;p&gt;I see a pattern that concerns me: developers who stop learning because AI can fill the gaps. They don&apos;t learn how promises work because AI writes their async code. They don&apos;t understand SQL joins because AI generates their queries. They don&apos;t learn design patterns because AI structures their code.&lt;/p&gt;
&lt;p&gt;This works until it doesn&apos;t. And when it doesn&apos;t — when you&apos;re debugging a race condition at 2 AM, or designing a system that needs to handle 10x traffic, or figuring out why the AI-generated code is causing a memory leak — there&apos;s no shortcut. You either understand the fundamentals or you don&apos;t.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI is a tool. You are the engineer. The tool doesn&apos;t replace the skill — it requires it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;AI in the developer workflow is genuinely useful. I&apos;m more productive with it than without it. That&apos;s not hype — that&apos;s my honest experience after extensive daily use.&lt;/p&gt;
&lt;p&gt;But the useful parts are the mundane parts. The boilerplate. The scaffolding. The exploration. The documentation. The stuff that was never the hard part of software engineering in the first place.&lt;/p&gt;
&lt;p&gt;The hard parts — understanding problems deeply, making architectural trade-offs, writing correct business logic, building secure systems, mentoring teams — those are still entirely human skills. And I don&apos;t see that changing anytime soon.&lt;/p&gt;
&lt;p&gt;Use AI tools. Get faster at the mechanical parts of development. But invest even more time in understanding the fundamentals. Because the developers who will thrive in an AI-augmented world aren&apos;t the ones who can prompt the best. They&apos;re the ones who can think the best.&lt;/p&gt;
&lt;p&gt;The AI is the power tool. You&apos;re still the craftsman. Don&apos;t forget which one matters more.&lt;/p&gt;
</content:encoded></item><item><title>Building Side Projects While Working Full-Time: A System, Not Motivation</title><link>https://hrdi.dev/blog/side-projects-while-working-fulltime/</link><guid isPermaLink="true">https://hrdi.dev/blog/side-projects-while-working-fulltime/</guid><description>Why motivation is unreliable and how building a system of small, consistent habits lets you actually ship side projects without burning out.</description><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Let me be honest with you: motivation is a terrible strategy for shipping side projects. I&apos;ve been building things on the side for years while holding a demanding full-time tech lead role, and the projects that actually shipped had nothing to do with bursts of inspiration. They were the result of boring, repeatable systems.&lt;/p&gt;
&lt;p&gt;If you&apos;re waiting to &quot;feel motivated&quot; to work on your side project after a long day, you&apos;ll be waiting forever. Here&apos;s what actually works.&lt;/p&gt;
&lt;h2&gt;Why Motivation Fails and Systems Win&lt;/h2&gt;
&lt;p&gt;Motivation is an emotion. It peaks after watching a YouTube video of someone who built a SaaS in a weekend and crashes the moment you hit a tricky database migration at 9 PM on a Tuesday. You can&apos;t build anything meaningful on emotions alone.&lt;/p&gt;
&lt;p&gt;Systems, on the other hand, are decisions you make once and then follow. They remove the need to decide every single day whether you &quot;feel like&quot; working. The decision is already made.&lt;/p&gt;
&lt;p&gt;My system is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two-hour evening blocks&lt;/strong&gt;, three nights a week (Tuesday, Wednesday, Thursday)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saturday mornings&lt;/strong&gt;, 7 AM to 11 AM — my most productive window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sunday is off.&lt;/strong&gt; No exceptions. Your brain needs recovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key insight: those two-hour blocks are sacred. I don&apos;t check Slack, I don&apos;t &quot;quickly&quot; fix a production bug, I don&apos;t doom-scroll. The block starts, I open my editor, and I work on the next task. That&apos;s it.&lt;/p&gt;
&lt;p&gt;Over six months, those blocks add up to roughly 400 hours. That&apos;s enough to ship a real product. Not a toy — a real, usable product.&lt;/p&gt;
&lt;h2&gt;Choosing Projects Strategically&lt;/h2&gt;
&lt;p&gt;Here&apos;s where most developers go wrong: they build what sounds cool instead of what solves a real problem. I&apos;ve been guilty of this too. I once spent three months building a &quot;revolutionary&quot; task manager that nobody needed, including me.&lt;/p&gt;
&lt;p&gt;The best side projects come from your own pain points. You already understand the problem deeply, you&apos;re your own first user, and you can validate instantly whether the solution works.&lt;/p&gt;
&lt;p&gt;Some of my best ideas came from friction I experienced at work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A legaltech tool born from watching lawyers manually process documents that could be automated in minutes&lt;/li&gt;
&lt;li&gt;An internal automation that saved my team hours of repetitive deployment tasks&lt;/li&gt;
&lt;li&gt;A monitoring dashboard because the existing tools were either too expensive or too complex for small teams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The filter I use now:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Does this problem cost me (or someone I know) real time or money?&lt;/li&gt;
&lt;li&gt;Can I build a usable V1 in under 8 weeks of side-project time?&lt;/li&gt;
&lt;li&gt;Am I genuinely interested enough to use this myself for months?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If any answer is no, I move on. There are infinite ideas. Your time is the scarce resource.&lt;/p&gt;
&lt;h2&gt;The MVP Mindset for Solo Builders&lt;/h2&gt;
&lt;p&gt;When you&apos;re a team of one, your MVP needs to be ruthlessly small. I&apos;m not talking about the startup pitch deck version of MVP. I mean: what is the absolute minimum thing that delivers value to one person (you)?&lt;/p&gt;
&lt;p&gt;For a SaaS I was building to automate contract review, my MVP was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload a PDF&lt;/li&gt;
&lt;li&gt;Extract key clauses using a simple NLP pipeline&lt;/li&gt;
&lt;li&gt;Display them in a list&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No user accounts. No payment system. No beautiful UI. Just the core value proposition working end to end. I built it in three weekends.&lt;/p&gt;
&lt;p&gt;Everything else — auth, billing, a proper frontend — came later, after I confirmed the core idea actually worked and people wanted it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# My MVP checklist before writing a single line of code:
1. Define the ONE thing this does
2. Sketch the happy path (literally on paper)
3. List what I&apos;m NOT building in V1
4. Set a deadline (usually 4-6 weekend sessions)
5. Ship it, even if it&apos;s ugly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &quot;not building&quot; list is more important than the feature list. It keeps you honest when scope creep whispers sweet nothings at 10 PM.&lt;/p&gt;
&lt;h2&gt;Tech Stack Decisions for Speed&lt;/h2&gt;
&lt;p&gt;This is where experienced developers actually have a disadvantage. We know too many tools, and we&apos;re tempted to pick the &quot;right&quot; one for every situation. For side projects, the right stack is the one you can move fastest with.&lt;/p&gt;
&lt;p&gt;My side project stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Go or Node.js with TypeScript — whichever fits the problem better&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Astro for content sites, Angular or React for apps (whatever I&apos;m most fluent in at the moment)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL. Always. I&apos;m not learning a new database at 9 PM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hosting:&lt;/strong&gt; Railway or Fly.io for backends, Vercel or Cloudflare Pages for frontends&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth:&lt;/strong&gt; Lucia or an external provider. Never roll your own&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The principle: &lt;strong&gt;minimize decisions that don&apos;t directly relate to your core problem.&lt;/strong&gt; Every hour spent debating ORMs is an hour not spent on the feature that matters.&lt;/p&gt;
&lt;p&gt;I&apos;ve seen developers spend weeks setting up the &quot;perfect&quot; monorepo with Nx, Turborepo, custom ESLint configs, and CI/CD pipelines — for a project that never shipped a single feature. Don&apos;t be that person.&lt;/p&gt;
&lt;h2&gt;Dealing with Burnout&lt;/h2&gt;
&lt;p&gt;Let&apos;s talk about the elephant in the room. Working full-time and building on the side is taxing. If you don&apos;t manage your energy intentionally, you&apos;ll burn out. I&apos;ve been there.&lt;/p&gt;
&lt;p&gt;Signs I&apos;ve learned to recognize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dreading the evening block instead of feeling neutral about it&lt;/li&gt;
&lt;li&gt;Writing code that I know is bad but not caring enough to fix it&lt;/li&gt;
&lt;li&gt;Skipping blocks for two weeks straight and making excuses&lt;/li&gt;
&lt;li&gt;Feeling resentful toward the side project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I notice these signs, I follow a strict protocol:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Take a full week off&lt;/strong&gt; from the side project. No guilt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reassess&lt;/strong&gt; whether I still care about the problem. If not, kill the project. Seriously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce the schedule&lt;/strong&gt; temporarily. Two blocks instead of three. One Saturday morning instead of the full session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Talk to someone&lt;/strong&gt; who isn&apos;t a developer. My partner&apos;s outside perspective has saved me from several pointless rabbit holes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal isn&apos;t to hustle until you break. The goal is to build consistently over years. You can&apos;t do that if you flame out every three months.&lt;/p&gt;
&lt;h2&gt;The Compound Effect&lt;/h2&gt;
&lt;p&gt;Here&apos;s what nobody tells you about side projects: the value isn&apos;t just the project itself. It&apos;s what accumulates around it.&lt;/p&gt;
&lt;p&gt;After two years of consistent side project work, I had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A portfolio of real, shipped products (not tutorial clones)&lt;/li&gt;
&lt;li&gt;Deep knowledge in areas outside my day job (legaltech, automation, NLP)&lt;/li&gt;
&lt;li&gt;A network of people who found my projects useful&lt;/li&gt;
&lt;li&gt;Battle-tested opinions about architecture, because I&apos;d made every mistake myself&lt;/li&gt;
&lt;li&gt;Confidence to make technical decisions faster, which directly improved my full-time work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each project taught me something that made the next one faster. My first SaaS attempt took six months and never launched. My most recent one went from idea to paying users in five weeks. Same number of evening blocks — just compounded experience.&lt;/p&gt;
&lt;h2&gt;A Closing Thought&lt;/h2&gt;
&lt;p&gt;Building side projects while working full-time isn&apos;t about being a &quot;hustle culture&quot; person. It&apos;s about having agency over your growth as an engineer and, potentially, your financial future.&lt;/p&gt;
&lt;p&gt;The developers I admire most aren&apos;t the ones who grind 80-hour weeks. They&apos;re the ones who show up consistently, build things that matter to them, and know when to rest.&lt;/p&gt;
&lt;p&gt;Set up your system. Protect your blocks. Ship something small. Then do it again.&lt;/p&gt;
&lt;p&gt;The motivation will come and go. The system stays.&lt;/p&gt;
</content:encoded></item><item><title>From Developer to Architect: The Mindset Shift Nobody Talks About</title><link>https://hrdi.dev/blog/developer-to-architect-mindset/</link><guid isPermaLink="true">https://hrdi.dev/blog/developer-to-architect-mindset/</guid><description>The transition from writing code to designing systems isn&apos;t about learning new tools — it&apos;s about fundamentally changing how you think about software.</description><pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There&apos;s a moment in every senior developer&apos;s career where you realize that writing great code isn&apos;t enough anymore. The problems you&apos;re facing can&apos;t be solved with a clever algorithm or a well-placed design pattern. They require you to zoom out — way out — and think about systems as a whole.&lt;/p&gt;
&lt;p&gt;That transition from developer to architect broke my brain for a while. Not because it&apos;s technically harder, but because it requires you to unlearn habits that made you successful as a developer. Here&apos;s what that shift actually looks like.&lt;/p&gt;
&lt;h2&gt;Thinking in Trade-offs, Not Absolutes&lt;/h2&gt;
&lt;p&gt;As a developer, I used to think in terms of &quot;the right way&quot; to do things. Normalized databases are better. Microservices are the future. Strong typing prevents bugs. These felt like universal truths.&lt;/p&gt;
&lt;p&gt;As an architect, I learned that almost nothing is universally true. Everything is a trade-off, and your job is to pick the trade-off that hurts the least for your specific context.&lt;/p&gt;
&lt;p&gt;Should you use microservices? It depends on your team size, deployment capabilities, and how well you understand your domain boundaries. Should you normalize your database? It depends on your read/write ratio and whether you can afford the join complexity at scale.&lt;/p&gt;
&lt;p&gt;The &quot;it depends&quot; reflex that junior developers mock is actually hard-earned wisdom. It means you&apos;ve seen enough systems succeed and fail to know that context determines everything.&lt;/p&gt;
&lt;p&gt;Here&apos;s a real example. On a project last year, I had to decide between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Event-driven architecture with Kafka — technically elegant, great for scale, complex to operate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Simple REST calls between services with a shared PostgreSQL database — &quot;boring,&quot; but the team could ship and maintain it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I chose Option B. The team was four developers, none had Kafka experience, and we had a three-month deadline. Option A would have been architecturally &quot;better&quot; in a vacuum. In our context, it would have been a disaster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The architect&apos;s job isn&apos;t to pick the best solution. It&apos;s to pick the best solution for this team, this timeline, and this set of constraints.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Danger of Ivory Tower Architecture&lt;/h2&gt;
&lt;p&gt;I&apos;ve worked with architects who design systems from a whiteboard and never look at the code. They produce beautiful diagrams, comprehensive ADRs, and architectures that fall apart the moment developers start implementing them.&lt;/p&gt;
&lt;p&gt;The best architects I know — the ones whose systems actually work in production — still read code daily. Not because they don&apos;t trust their teams, but because code is where architecture meets reality.&lt;/p&gt;
&lt;p&gt;When I review pull requests, I&apos;m not looking for style issues or variable names. I&apos;m looking for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are the boundaries I designed actually holding, or are developers working around them?&lt;/li&gt;
&lt;li&gt;Is the abstraction I proposed actually reducing complexity, or adding it?&lt;/li&gt;
&lt;li&gt;Are there patterns emerging in the code that suggest the architecture needs to evolve?&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// This is a smell I look for in code reviews.
// When you see a service directly importing another service&apos;s
// internal types, the architectural boundary has been violated.

import (
    &quot;github.com/company/user-service/internal/models&quot;  // RED FLAG
    &quot;github.com/company/order-service/domain&quot;
)

// What it should look like:
import (
    &quot;github.com/company/shared/contracts&quot;  // Shared interface
    &quot;github.com/company/order-service/domain&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your architecture can&apos;t survive contact with real developers writing real code under real deadlines, it&apos;s not architecture — it&apos;s fiction.&lt;/p&gt;
&lt;h2&gt;Communication Is the Architect&apos;s Primary Tool&lt;/h2&gt;
&lt;p&gt;This one blindsided me. I thought becoming an architect meant spending more time on technical problems. In reality, I spend more time communicating than coding.&lt;/p&gt;
&lt;p&gt;An architecture decision that isn&apos;t understood by the team is worthless. You need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Explain the &quot;why&quot;&lt;/strong&gt; behind every significant decision. Not just what we&apos;re doing, but why this approach over the alternatives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create shared mental models.&lt;/strong&gt; When I say &quot;bounded context,&quot; every developer on the team should picture the same thing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Listen to pushback.&lt;/strong&gt; Developers on the ground see things you can&apos;t see from the architecture level. If three developers tell you the abstraction is painful, believe them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write it down.&lt;/strong&gt; Architecture Decision Records (ADRs) aren&apos;t bureaucracy — they&apos;re your team&apos;s memory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I once designed what I thought was an elegant event sourcing system. During the design review, a mid-level developer asked: &quot;How do we debug this when something goes wrong at 3 AM?&quot; I didn&apos;t have a good answer. That question saved us months of pain. The developer didn&apos;t need to understand event sourcing theory — they needed to understand operational reality. And they were right.&lt;/p&gt;
&lt;h2&gt;How DDD Changed My Thinking&lt;/h2&gt;
&lt;p&gt;Domain-Driven Design was the single biggest influence on my transition to architectural thinking. Not because of the tactical patterns — aggregates, repositories, value objects — but because of the strategic ones.&lt;/p&gt;
&lt;p&gt;DDD taught me to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with the domain, not the technology.&lt;/strong&gt; Before I think about databases or frameworks, I spend time understanding the business problem. What are the entities? What are the processes? Where are the boundaries?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Speak the domain language.&lt;/strong&gt; If the business calls it a &quot;policy&quot; and your code calls it a &quot;contract,&quot; you&apos;ve created a translation layer that will cause bugs and miscommunication forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Identify bounded contexts.&lt;/strong&gt; This is the most powerful concept in DDD. Not everything needs to share the same model. The &quot;User&quot; in the billing context is different from the &quot;User&quot; in the social context, and that&apos;s not just okay — it&apos;s correct.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Before DDD thinking:
One giant User model with 47 fields serving 6 different contexts

# After DDD thinking:
- Billing::Customer (email, payment_method, subscription_tier)
- Social::Profile (display_name, avatar, bio)
- Auth::Identity (credentials, mfa_settings, sessions)
- Support::Requester (name, contact_info, ticket_history)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each context owns its model. They communicate through well-defined interfaces. Changes in one don&apos;t cascade into others. This isn&apos;t over-engineering — it&apos;s recognizing that a single model serving everyone actually serves no one well.&lt;/p&gt;
&lt;h2&gt;Making Decisions with Incomplete Information&lt;/h2&gt;
&lt;p&gt;As a developer, I could gather all the requirements, understand the full scope, and then build. As an architect, I almost never have complete information when I need to make decisions.&lt;/p&gt;
&lt;p&gt;You&apos;re choosing a messaging system before you know the final scale requirements. You&apos;re designing service boundaries before the business model is fully validated. You&apos;re picking a cloud provider before you know which regions you&apos;ll need.&lt;/p&gt;
&lt;p&gt;This used to paralyze me. What if I&apos;m wrong? At this scale, being wrong is expensive.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I&apos;ve learned: &lt;strong&gt;the cost of not deciding is almost always higher than the cost of a wrong decision.&lt;/strong&gt; A wrong decision can be corrected. Indecision blocks entire teams.&lt;/p&gt;
&lt;p&gt;My framework for decisions under uncertainty:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Make the decision reversible if possible.&lt;/strong&gt; Use abstractions that let you swap implementations later. This is where ports and adapters architecture shines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set a review date.&lt;/strong&gt; &quot;We&apos;re going with PostgreSQL for now. In three months, we&apos;ll evaluate if we need to move to a distributed database based on actual load data&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document your assumptions.&lt;/strong&gt; When you write down &quot;We assume fewer than 10,000 concurrent users in Year 1,&quot; you create a tripwire that tells future-you when the decision needs revisiting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accept that some decisions will be wrong.&lt;/strong&gt; Design your system so that wrong decisions don&apos;t bring everything down. Loose coupling isn&apos;t just a buzzword — it&apos;s insurance&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Fear of Being Wrong at Scale&lt;/h2&gt;
&lt;p&gt;When you&apos;re a developer and you write a bug, it affects one feature. Maybe a few users see an error message. You fix it, deploy, and move on.&lt;/p&gt;
&lt;p&gt;When you&apos;re an architect and you make a bad structural decision, it can affect every team, every service, and every deployment for months or years. That weight is real, and pretending it doesn&apos;t exist doesn&apos;t help.&lt;/p&gt;
&lt;p&gt;I deal with it by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building proof-of-concepts&lt;/strong&gt; before committing to major architectural decisions. A two-week spike is cheap compared to a six-month rewrite&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting input from other experienced engineers.&lt;/strong&gt; Architecture reviews aren&apos;t a sign of weakness — they&apos;re a sign of maturity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Studying failures.&lt;/strong&gt; I learn more from post-mortems than from success stories. When a system fails, the architecture&apos;s weaknesses become visible&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;From &quot;I&apos;ll Build It&quot; to &quot;I&apos;ll Design It So Others Can Build It&quot;&lt;/h2&gt;
&lt;p&gt;The hardest part of this transition was letting go of the code. As a developer, my identity was tied to what I built. As an architect, my value comes from enabling others to build well.&lt;/p&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing clear specifications instead of writing the code yourself&lt;/li&gt;
&lt;li&gt;Trusting your team to make implementation decisions within the boundaries you&apos;ve set&lt;/li&gt;
&lt;li&gt;Measuring success by team output, not personal output&lt;/li&gt;
&lt;li&gt;Being okay with solutions that aren&apos;t how you would have coded them, as long as they respect the architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s a different kind of satisfaction. Instead of looking at a feature and thinking &quot;I built that,&quot; you look at a system and think &quot;I designed the structure that let twelve people build that without stepping on each other.&quot;&lt;/p&gt;
&lt;h2&gt;A Closing Reflection&lt;/h2&gt;
&lt;p&gt;The transition from developer to architect isn&apos;t a promotion — it&apos;s a career change. The skills that made you a great developer (deep focus, attention to detail, hands-on problem solving) don&apos;t go away, but they become supporting skills rather than primary ones.&lt;/p&gt;
&lt;p&gt;Your primary skills become: systems thinking, communication, decision-making under uncertainty, and the humility to know that your beautiful architecture will be bent, broken, and patched by reality.&lt;/p&gt;
&lt;p&gt;And honestly? That&apos;s what makes it fascinating. You&apos;re not building software anymore. You&apos;re designing the environment where software gets built. Get that right, and everything else follows.&lt;/p&gt;
</content:encoded></item><item><title>What Training for a Half Marathon Taught Me About Engineering</title><link>https://hrdi.dev/blog/discipline-training-engineering/</link><guid isPermaLink="true">https://hrdi.dev/blog/discipline-training-engineering/</guid><description>Parallels between hybrid athletic training and software engineering — progressive overload, rest days, hitting the wall, and why consistency always beats intensity.</description><pubDate>Fri, 30 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The Starting Line Nobody Sees&lt;/h2&gt;
&lt;p&gt;I signed up for my first half marathon thinking it would be a fun weekend thing. Something to check off a list. What I didn&apos;t expect was how deeply the training would mirror the exact challenges I face every day as a Tech Lead building production systems.&lt;/p&gt;
&lt;p&gt;I&apos;ve been doing hybrid training — gym work three days a week combined with running progressions from 10K to 21K — for over a year now. And somewhere around month four, I started noticing that almost every principle that made me a better runner also made me a better engineer. Not in a cute, motivational-poster way. In a real, practical, &quot;this changes how I plan sprints&quot; way.&lt;/p&gt;
&lt;p&gt;Let me break it down.&lt;/p&gt;
&lt;h2&gt;Progressive Overload = Incremental Complexity&lt;/h2&gt;
&lt;p&gt;In training, progressive overload means you don&apos;t jump from running 5K to 21K in a week. You add 10% volume per week. You earn the next level.&lt;/p&gt;
&lt;p&gt;In engineering, this is the same principle behind incremental delivery. You don&apos;t go from a monolith to microservices in one sprint. You extract one bounded context, validate it works, then extract the next. Every time I&apos;ve seen a team try to &quot;big bang&quot; a migration, it ended in tears. Every single time.&lt;/p&gt;
&lt;p&gt;The body adapts to stress gradually. So does a codebase. So does a team.&lt;/p&gt;
&lt;p&gt;When I onboard new engineers, I give them progressively harder tasks — not because I&apos;m gatekeeping, but because context builds on context. You need to understand the data model before you touch the event pipeline. Progressive overload isn&apos;t just a training principle. It&apos;s a leadership principle.&lt;/p&gt;
&lt;h2&gt;Rest Days = Preventing Burnout&lt;/h2&gt;
&lt;p&gt;Here&apos;s something gym culture gets wrong and tech culture gets even more wrong: rest is not laziness. Rest is where adaptation happens.&lt;/p&gt;
&lt;p&gt;Your muscles don&apos;t grow during the workout. They grow during recovery. Your best engineering ideas don&apos;t come during a 12-hour coding session. They come in the shower the next morning, after your brain has had time to process.&lt;/p&gt;
&lt;p&gt;I enforce rest days on my training schedule with the same discipline I enforce sustainable pace on my teams. No hero sprints. No &quot;let&apos;s just push through the weekend.&quot; I&apos;ve seen what happens when you skip rest days in training — you get injured. I&apos;ve seen what happens when teams skip rest — you get attrition, bugs, and people who stop caring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; If your team is consistently working overtime, you don&apos;t have a capacity problem. You have a planning problem.&lt;/p&gt;
&lt;h2&gt;The Training Plan = The Sprint Plan&lt;/h2&gt;
&lt;p&gt;A good training plan has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Periodization&lt;/strong&gt; — cycles of building up and tapering down&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Specificity&lt;/strong&gt; — workouts that target the actual race demands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurable milestones&lt;/strong&gt; — time trials, distance markers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt; — adjustments based on how the body responds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Replace &quot;workouts&quot; with &quot;tasks&quot; and &quot;race&quot; with &quot;release&quot; and you have a solid sprint planning philosophy. The best sprint plans I&apos;ve run have all of these elements. The worst ones were rigid, unrealistic, and ignored feedback from the team.&lt;/p&gt;
&lt;p&gt;I used to plan sprints like a beginner plans training: stuff as much as possible into each week. Now I plan like an experienced runner: leave room for the unexpected, build in recovery, and always know what the priority workout is for that week.&lt;/p&gt;
&lt;h2&gt;Consistency Beats Intensity&lt;/h2&gt;
&lt;p&gt;This one changed everything for me.&lt;/p&gt;
&lt;p&gt;Running 5K four times a week will make you faster than running 15K once a week and collapsing. Deploying small, well-tested changes daily will get you further than massive releases every two weeks.&lt;/p&gt;
&lt;p&gt;In training, consistency builds your aerobic base. In engineering, consistency builds deployment confidence. The teams I lead that ship small and often have fewer incidents, faster recovery times, and — here&apos;s the counterintuitive part — more total output than teams that batch everything into big releases.&lt;/p&gt;
&lt;p&gt;The same applies to learning. Reading 30 minutes of documentation every day beats a weekend binge. Writing one small PR a day beats a 2,000-line monster on Friday afternoon.&lt;/p&gt;
&lt;h2&gt;The Wall at KM 18 = The Wall at Month 6&lt;/h2&gt;
&lt;p&gt;Every distance runner knows &quot;the wall.&quot; For a half marathon, it usually hits around kilometer 18. Your glycogen is depleted, your legs are screaming, and your brain is looking for any excuse to stop.&lt;/p&gt;
&lt;p&gt;In long-running projects, there&apos;s a similar wall. Around month 5 or 6, the initial excitement is gone. The hard architectural problems have been solved, but now you&apos;re grinding through integrations, edge cases, and the kind of work nobody celebrates. Team morale dips. People start questioning decisions made in month 1.&lt;/p&gt;
&lt;p&gt;How you handle the wall in training:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You expected it.&lt;/strong&gt; It&apos;s part of the plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You have nutrition strategy.&lt;/strong&gt; Gels, hydration, pacing adjustments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You focus on the next kilometer, not the finish line.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How I handle the wall on projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I warn the team early.&lt;/strong&gt; &quot;Months 5-6 will be tough. That&apos;s normal.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I break the remaining work into visible, completable milestones.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I celebrate small wins publicly.&lt;/strong&gt; Shipped the notification service? That&apos;s a win. Say it out loud.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mental game is real. In both arenas.&lt;/p&gt;
&lt;h2&gt;Tracking Metrics: Pace/HR = Observability/Monitoring&lt;/h2&gt;
&lt;p&gt;In running, I track pace, heart rate, cadence, and heart rate variability. Not because I&apos;m obsessed with numbers, but because without data, I&apos;m just guessing whether I&apos;m improving or overtraining.&lt;/p&gt;
&lt;p&gt;In production, we track latency, error rates, throughput, and saturation. Same reason.&lt;/p&gt;
&lt;p&gt;Here&apos;s what both have taught me about metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vanity metrics are dangerous.&lt;/strong&gt; Total kilometers run doesn&apos;t tell you if you&apos;re getting faster. Total deploys doesn&apos;t tell you if you&apos;re shipping value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trends matter more than snapshots.&lt;/strong&gt; One bad run doesn&apos;t mean anything. One spike in error rate doesn&apos;t mean anything. But a three-week downward trend in both? Pay attention.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need baselines.&lt;/strong&gt; My resting heart rate is 52 bpm. If it jumps to 60, something is wrong — even if 60 is &quot;normal&quot; for someone else. Same with your p99 latency. Know YOUR normal.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Your observability should tell a story, not just show numbers
// Bad: alert when errors &amp;gt; 100
// Good: alert when error_rate deviates &amp;gt; 2σ from 7-day baseline

alert:
  name: error_rate_anomaly
  condition: |
    abs(current_error_rate - avg_error_rate_7d) &amp;gt; 2 * stddev_error_rate_7d
  severity: warning
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Injury Prevention = Tech Debt Management&lt;/h2&gt;
&lt;p&gt;Runners who skip mobility work, ignore minor pains, and never deload eventually get injured. It&apos;s not a question of if, it&apos;s when.&lt;/p&gt;
&lt;p&gt;Engineers who skip refactoring, ignore code smells, and never pay down tech debt eventually face a production incident caused by accumulated neglect. Same certainty.&lt;/p&gt;
&lt;p&gt;The parallel is almost too clean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stretching and foam rolling&lt;/strong&gt; = regular refactoring sessions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ignoring knee pain&lt;/strong&gt; = ignoring flaky tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running on a stress fracture&lt;/strong&gt; = shipping on top of known critical bugs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proper shoes&lt;/strong&gt; = proper tooling and infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I schedule &quot;deload weeks&quot; for my teams — sprints where we deliberately reduce feature work and focus on tooling, test coverage, dependency updates, and refactoring. It&apos;s not lost productivity. It&apos;s injury prevention.&lt;/p&gt;
&lt;h2&gt;The Mental Game&lt;/h2&gt;
&lt;p&gt;The hardest part of running 21K isn&apos;t physical. By race day, your body is ready. The hard part is the voice in your head at kilometer 15 saying &quot;you could just walk.&quot;&lt;/p&gt;
&lt;p&gt;The hardest part of leading a long project isn&apos;t technical. Your team is skilled enough. The hard part is maintaining conviction when stakeholders are anxious, when the competitor shipped something flashy, when the easy shortcut is right there.&lt;/p&gt;
&lt;p&gt;In both cases, the answer is the same: &lt;strong&gt;trust the training.&lt;/strong&gt; Trust the process you built. Trust the plan. Trust that consistency compounds. And when it gets hard — and it will get hard — remember that you prepared for this moment specifically.&lt;/p&gt;
&lt;h2&gt;Crossing the Finish Line&lt;/h2&gt;
&lt;p&gt;I finished my first half marathon in 1:52. Not elite. Not record-breaking. But I finished. And I finished because I respected the process, listened to my body, stayed consistent, and didn&apos;t quit at kilometer 18.&lt;/p&gt;
&lt;p&gt;Every successful project I&apos;ve led followed the same pattern. Not glamorous. Not revolutionary. Just consistent effort, honest assessment, planned recovery, and the discipline to keep going when it stopped being fun.&lt;/p&gt;
&lt;p&gt;The finish line — whether it&apos;s a race or a release — is just the visible result of months of invisible work. And that invisible work? That&apos;s where the real engineering happens.&lt;/p&gt;
&lt;p&gt;Lace up. Ship small. Stay consistent. The wall is coming — and you&apos;re ready for it.&lt;/p&gt;
</content:encoded></item><item><title>Lessons From Running Production Systems With Distributed Teams</title><link>https://hrdi.dev/blog/lessons-distributed-teams-production/</link><guid isPermaLink="true">https://hrdi.dev/blog/lessons-distributed-teams-production/</guid><description>Real lessons from managing production systems across timezones — observability, incident management, runbooks, deployment confidence, and building a culture of ownership.</description><pubDate>Sun, 25 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;The 3 AM Alert That Changed How I Think About Teams&lt;/h2&gt;
&lt;p&gt;I was deep asleep when the PagerDuty alert hit. An e-commerce checkout service was throwing 500s at a rate that would make your monitoring dashboard look like a seismograph. The service ran on EKS, the queue was backed up in SQS, and our team was spread across three timezones — Ecuador, Argentina, and Spain.&lt;/p&gt;
&lt;p&gt;That night taught me more about distributed systems AND distributed teams than any architecture book ever could. Because when production is on fire, it doesn&apos;t care where your team lives.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I&apos;ve learned from years of running production systems with teams that never share an office.&lt;/p&gt;
&lt;h2&gt;Observability Is the Great Equalizer&lt;/h2&gt;
&lt;p&gt;When your team is distributed, nobody can tap a colleague on the shoulder and ask &quot;hey, does this look weird to you?&quot; Observability fills that gap. It becomes the shared language your team speaks across timezones.&lt;/p&gt;
&lt;p&gt;We standardized on OpenTelemetry for instrumentation and structured logging everywhere. Not because it&apos;s trendy — because when an engineer in Argentina picks up an incident at 9 AM their time, they need to see the SAME context that the engineer in Ecuador saw at 2 AM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What actually works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structured logging with correlation IDs.&lt;/strong&gt; Every request gets a trace ID that flows through every service, every queue message, every database call. When something breaks, you follow one ID through the entire system. No guessing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business-level metrics, not just infra metrics.&lt;/strong&gt; CPU usage is nice. &quot;Orders per minute dropped 40% in the last 5 minutes&quot; is actionable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dashboards that tell stories.&lt;/strong&gt; We build dashboards around user journeys, not services. The &quot;Checkout Health&quot; dashboard shows everything from cart creation to payment confirmation in one view.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Structured logging that actually helps during incidents
// Bad: logger.error(&quot;Payment failed&quot;)
// Good:
logger.error({
  event: &apos;payment_processing_failed&apos;,
  orderId: order.id,
  paymentProvider: &apos;stripe&apos;,
  errorCode: err.code,
  amount: order.total,
  currency: order.currency,
  traceId: context.traceId,
  customerSegment: order.customer.tier,
  retryAttempt: attempt,
  latencyMs: Date.now() - startTime,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference between those two log lines is the difference between &quot;something broke&quot; and &quot;I know exactly what broke, for whom, and how to fix it.&quot;&lt;/p&gt;
&lt;h2&gt;Incident Management Across Timezones&lt;/h2&gt;
&lt;p&gt;Incident management with a distributed team requires explicit structure. You can&apos;t rely on &quot;everyone jumps on a call&quot; when it&apos;s 3 AM for half your team.&lt;/p&gt;
&lt;p&gt;Here&apos;s the system we built:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Follow-the-sun on-call rotation.&lt;/strong&gt; Each timezone has primary and secondary on-call. The incident stays with whoever picks it up until it&apos;s resolved OR explicitly handed off with a written summary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incident channels, not threads.&lt;/strong&gt; Every P1/P2 gets a dedicated Slack channel with a bot that timestamps every update. When the next timezone wakes up, they read the channel top to bottom and know exactly where things stand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 15-minute rule.&lt;/strong&gt; If you can&apos;t identify root cause in 15 minutes, escalate. Not because you&apos;re incompetent — because you&apos;re managing time, not ego. The sooner you pull in help, the sooner users are unblocked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The handoff is the hardest part. We use a template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Incident Handoff — [INC-2024-0847]
**Status:** Investigating / Mitigated / Monitoring
**Impact:** ~12% of checkout attempts failing in US-East
**What we know:** SQS consumer lag spiking on payment-processor queue
**What we&apos;ve tried:** Scaled consumers to 8 replicas (no improvement)
**Current hypothesis:** Stripe webhook endpoint is rate-limiting us
**Next steps:** Check Stripe dashboard, consider circuit breaker activation
**Key links:** [Dashboard](link) | [Logs query](link) | [Trace](link)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That template has saved us hours of &quot;wait, what happened while I was asleep?&quot;&lt;/p&gt;
&lt;h2&gt;Runbooks That Actually Get Used&lt;/h2&gt;
&lt;p&gt;Most runbooks are graveyard documents. Written once, never updated, never consulted during an actual incident. I&apos;ve been guilty of this. Here&apos;s what changed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runbooks work when they&apos;re:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linked directly from alerts.&lt;/strong&gt; Every PagerDuty alert includes a runbook link. You don&apos;t search for it — it&apos;s right there in the notification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Written as decision trees, not novels.&lt;/strong&gt; &quot;If metric X is above Y, do Z.&quot; Not three paragraphs of background context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tested regularly.&lt;/strong&gt; We run &quot;runbook drills&quot; monthly. Pick a scenario, follow the runbook, see if it still works. If it doesn&apos;t, update it on the spot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Owned by the on-call team.&lt;/strong&gt; The people who use runbooks are the ones who maintain them. Not a separate documentation team.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# runbook: payment-queue-lag
trigger: sqs_queue_depth &amp;gt; 10000 for 5m
steps:
  - check: &quot;Are consumers running?&quot;
    command: &quot;kubectl get pods -l app=payment-consumer -n production&quot;
    if_no: &quot;kubectl rollout restart deployment/payment-consumer -n production&quot;
    if_yes: &quot;continue&quot;
  - check: &quot;Is Stripe responding?&quot;
    command: &quot;curl -o /dev/null -s -w &apos;%{http_code}&apos; https://api.stripe.com/v1/health&quot;
    if_5xx: &quot;Activate circuit breaker: kubectl set env deployment/payment-consumer STRIPE_CIRCUIT=open&quot;
    if_200: &quot;continue&quot;
  - check: &quot;Consumer error logs&quot;
    command: &quot;kubectl logs -l app=payment-consumer --tail=100 | grep ERROR&quot;
    action: &quot;Analyze error pattern and escalate if unknown&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&quot;You Build It, You Run It&quot; — In Practice&lt;/h2&gt;
&lt;p&gt;This philosophy sounds great in conference talks. In practice, with a distributed team, it requires infrastructure.&lt;/p&gt;
&lt;p&gt;You can&apos;t expect developers to &quot;run&quot; what they build if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They don&apos;t have access to production logs&lt;/li&gt;
&lt;li&gt;They can&apos;t deploy without filing a ticket&lt;/li&gt;
&lt;li&gt;Alerts go to a separate ops team instead of the building team&lt;/li&gt;
&lt;li&gt;There&apos;s no way to safely roll back their changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What we actually implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every team owns their service&apos;s alerts, dashboards, and runbooks.&lt;/strong&gt; Not ops. Not SRE. The team that writes the code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment pipelines with automated canary analysis.&lt;/strong&gt; Engineers deploy their own code. The pipeline watches error rates for 10 minutes. If they spike, it rolls back automatically. This gives developers deployment confidence without requiring an ops gatekeeper.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature flags for everything user-facing.&lt;/strong&gt; Deploy code anytime. Activate features during business hours when the team is awake and watching.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# Canary deployment strategy on EKS
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: checkout-service
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: checkout-service
  progressDeadlineSeconds: 600
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
      - name: request-success-rate
        thresholdRange:
          min: 99
        interval: 1m
      - name: request-duration
        thresholdRange:
          max: 500
        interval: 1m
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Async Communication Patterns That Work&lt;/h2&gt;
&lt;p&gt;Distributed teams live and die by their async communication. Meetings across timezones are expensive and often unnecessary. Here&apos;s what replaced most of ours:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ADRs (Architecture Decision Records) for every significant decision.&lt;/strong&gt; Not because we love documentation — because when your teammate in Spain wakes up, they need to understand WHY we chose SQS over Kafka, not just that we did.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Video Looms instead of meetings.&lt;/strong&gt; Five-minute video walkthrough of a PR or a design doc. The receiver watches at 1.5x on their schedule. Response in writing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RFC documents with explicit deadlines.&lt;/strong&gt; &quot;Comments due by Thursday EOD UTC.&quot; No ambiguity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daily async standups in Slack.&lt;/strong&gt; Three lines: what I did, what I&apos;m doing, any blockers. Posted when your day starts. Read when theirs does.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rule of thumb: &lt;strong&gt;if the information needs to survive longer than the meeting, it should never have been a meeting.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Deployment Confidence Problem&lt;/h2&gt;
&lt;p&gt;The scariest moment in distributed team operations is deploying on a Friday afternoon when the team that built the feature is about to go offline for the weekend.&lt;/p&gt;
&lt;p&gt;We solved this with layers of confidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature flags&lt;/strong&gt; — deploy code dark, activate when the team is online&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Canary releases&lt;/strong&gt; — automated traffic shifting with automatic rollback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment windows&lt;/strong&gt; — we deploy anytime, but activations happen during overlapping hours (our &quot;golden window&quot; is 10 AM - 1 PM UTC when all timezones are awake)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SNS notifications&lt;/strong&gt; — every deployment triggers a notification to the team channel with commit summary, author, and rollback instructions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn&apos;t to slow down deployments. It&apos;s to make every deployment boring. Boring deployments mean your system is healthy.&lt;/p&gt;
&lt;h2&gt;War Story: The Silent Queue&lt;/h2&gt;
&lt;p&gt;One Saturday, our order processing queue (SQS) stopped consuming messages. No errors. No alerts. Just... silence. Orders were piling up, but everything looked green on our dashboards.&lt;/p&gt;
&lt;p&gt;The root cause? A Kubernetes node had been cordoned during maintenance, the consumer pods were evicted, and the Horizontal Pod Autoscaler had a minimum replica count of zero. So when the pods were evicted, the HPA was perfectly happy with zero replicas processing zero messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What we changed after:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimum replica count for critical consumers is now 2. Always.&lt;/li&gt;
&lt;li&gt;We added a &quot;zero consumer&quot; alert: if any critical SQS queue has zero active consumers for more than 2 minutes, page immediately.&lt;/li&gt;
&lt;li&gt;Runbook updated same day.&lt;/li&gt;
&lt;li&gt;Post-mortem shared across all teams, not just the affected one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No blame. Just learning.&lt;/p&gt;
&lt;h2&gt;Building a Culture of Ownership, Not Blame&lt;/h2&gt;
&lt;p&gt;Blameless post-mortems aren&apos;t just a nice idea. They&apos;re a survival mechanism for distributed teams. Here&apos;s why: if people fear blame, they hide problems. If they hide problems, problems grow. If problems grow across timezones, you find out when it&apos;s already a disaster.&lt;/p&gt;
&lt;p&gt;Our post-mortem template has one rule: &lt;strong&gt;no human names in the root cause section.&lt;/strong&gt; The system failed. Not a person.&lt;/p&gt;
&lt;p&gt;We focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What controls were missing that allowed this to happen?&lt;/li&gt;
&lt;li&gt;What signals did we miss and why?&lt;/li&gt;
&lt;li&gt;What automation would have caught this before users noticed?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result? Engineers proactively report near-misses. They flag &quot;this feels fragile&quot; without fear. And the whole team gets better because information flows freely.&lt;/p&gt;
&lt;h2&gt;Documentation Is a Production Dependency&lt;/h2&gt;
&lt;p&gt;I&apos;ll say it plainly: if your system can&apos;t be operated by someone who wasn&apos;t in the room when it was built, your documentation is failing. And with distributed teams, someone is ALWAYS not in the room.&lt;/p&gt;
&lt;p&gt;We treat documentation like we treat tests. It&apos;s not optional. It&apos;s not &quot;nice to have.&quot; It&apos;s a production dependency. If you ship a new service without an updated README, operational runbook, and architecture diagram, your PR doesn&apos;t get merged.&lt;/p&gt;
&lt;p&gt;Is it extra work? Yes. Does it pay for itself the first time someone in a different timezone needs to debug your service at 3 AM? Absolutely.&lt;/p&gt;
&lt;h2&gt;The Honest Truth&lt;/h2&gt;
&lt;p&gt;Running production systems with distributed teams is harder than doing it in a co-located office. Anyone who tells you otherwise is selling something. But it&apos;s also possible, sustainable, and — when you get it right — incredibly rewarding.&lt;/p&gt;
&lt;p&gt;The key isn&apos;t fancy tools or complex processes. It&apos;s building systems (both technical and human) that assume someone is always sleeping, someone is always debugging, and the documentation is always the first thing they&apos;ll read.&lt;/p&gt;
&lt;p&gt;Make that experience great, and everything else follows.&lt;/p&gt;
</content:encoded></item></channel></rss>