<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohammad Jawad (Kasir) Barati</title>
    <description>The latest articles on DEV Community by Mohammad Jawad (Kasir) Barati (@kasir-barati).</description>
    <link>https://dev.to/kasir-barati</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F595495%2F146d2ca6-6004-437f-9be2-8edaa5a35d34.png</url>
      <title>DEV Community: Mohammad Jawad (Kasir) Barati</title>
      <link>https://dev.to/kasir-barati</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kasir-barati"/>
    <language>en</language>
    <item>
      <title>Monorepo vs. Polyrepo</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Thu, 10 Sep 2026 01:12:57 +0000</pubDate>
      <link>https://dev.to/kasir-barati/monorepo-vs-polyrepo-2e40</link>
      <guid>https://dev.to/kasir-barati/monorepo-vs-polyrepo-2e40</guid>
      <description>&lt;p&gt;I had an technical debate about when one should "just put it all in one monorepo", and honestly at that time I was on the side of monorepo for the project in question but did not know how to reason about it. I mean I was not sure how it pays off. So that is why I did a little bit of thinking and realized we do not need to answer "are these projects related?". Almost everything in a product is related 😉.&lt;/p&gt;

&lt;p&gt;Rather the question is: &lt;strong&gt;do these projects share a toolchain, and does that sharing actually reduce friction?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here I decided to use my two repos from the same product to make a useful A/B test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel" rel="noopener noreferrer"&gt;smart-novel&lt;/a&gt; is managed by Nx monorepo and it houses the frontend and backend&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel-beatrice" rel="noopener noreferrer"&gt;smart-novel-beatrice&lt;/a&gt; is a standalone Python service (Beatrice), split out on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Frontend and Backend Live Together
&lt;/h2&gt;

&lt;p&gt;Both apps in &lt;code&gt;smart-novel&lt;/code&gt; are TypeScript. That single fact cascades into a lot of shared infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One dependency graph, one lockfile, one version of TypeScript/ESLint/Prettier across both apps.&lt;/li&gt;
&lt;li&gt;Nx can build, test, and lint only what changed, across app boundaries, in a single command.&lt;/li&gt;
&lt;li&gt;Shared types and utilities can be imported directly instead of published as packages.&lt;/li&gt;
&lt;li&gt;One CI pipeline, one set of environment conventions, one place to look for config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires the frontend and backend to be &lt;em&gt;conceptually&lt;/em&gt; simple or tightly coupled, it requires them to speak the same toolchain. Nx's whole value proposition is coordinating a graph of packages that already share a runtime and build system. Put differently: the monorepo isn't paying for "frontend and backend are part of the same product", it's paying for "frontend and backend are both TypeScript projects that Nx can reason about together."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Beatrice Doesn't
&lt;/h2&gt;

&lt;p&gt;Beatrice is Python, it comes with &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;uv.lock&lt;/code&gt;. It has its own linters, test runner, CI/CD. None of that has anything in common with Nx's build graph. Folding it into the Nx repo wouldn't add coordination, it would add friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nx has nothing to orchestrate for a Python package. It's dead weight in the workspace config.&lt;/li&gt;
&lt;li&gt;A Python contributor now has to understand an Nx workspace just to find &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;CI has to be able to work with a Python project inside a workspace tuned for another language's tooling.&lt;/li&gt;
&lt;li&gt;Versioning and release cadence for a Python service don't necessarily track the frontend/backend release cadence anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the separation isn't a judgment that Beatrice is "less important" or unrelated to the product, it's that gluing it to a toolchain built for a different language fights the tooling rather than helping iteration speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Criteria
&lt;/h2&gt;

&lt;p&gt;Boiled down, the questions that decide "same repo or separate repo" are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do they share a build toolchain and language runtime?&lt;/strong&gt; If yes, a monorepo tool like Nx can add real value; shared dependency graphs, affected-only builds, one lint/format config. If no, the tool has nothing to coordinate and just adds overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Would merging them require one project to route around the other's tooling?&lt;/strong&gt; If a Python service has to sit inside a Node build graph (or vice versa), you've added a foreign-language exception to every CI/CD script, editor config, and onboarding doc, that's cost, not synergy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do they release and version together in practice?&lt;/strong&gt; Two apps deployed in lockstep benefit from atomic commits across both. Two services with independent release cadences don't need that coupling, and a shared repo can make it harder to see which one actually changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does "related product" already imply "related tooling"?&lt;/strong&gt; It often doesn't. Two parts of the same product can legitimately be written in different languages for good reasons (ML tooling in Python, application layer in TypeScript), that's a reason to keep them separate, not a coincidence to design around.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs7p9w3u9k9p053hqvrl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzs7p9w3u9k9p053hqvrl.png" alt="Monorepo VS polyrepo in a nutshell" width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monorepo</category>
      <category>polyrepo</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Observability &amp; Telemetry Retention Policy</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Thu, 10 Sep 2026 00:11:16 +0000</pubDate>
      <link>https://dev.to/kasir-barati/observability-telemetry-retention-policy-1ene</link>
      <guid>https://dev.to/kasir-barati/observability-telemetry-retention-policy-1ene</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;OpenTelemetry (OTel)&lt;/strong&gt; for application observability and start with &lt;strong&gt;Grafana Cloud Free&lt;/strong&gt; as the backend. I love both of them. They offer everything you will be needing when you have a bug ticket.&lt;/p&gt;

&lt;p&gt;Grafana Cloud Free currently provides &lt;strong&gt;14-day retention&lt;/strong&gt; for metrics, logs, traces, profiles, and k6 performance tests, with 50 GB each of logs and traces included. It is explicitly intended for personal projects and early-stage startups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;14 days is sufficient for most applications in beta/MVP.&lt;/strong&gt; Incidents usually are investigated within hours or days, not months. Increase retention only when there is a demonstrated operational, business, security, or compliance need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Think About Telemetry in Layers
&lt;/h2&gt;

&lt;p&gt;Not all telemetry needs the same retention period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌──────────────────────────┐
                 │      Long-lived data     │
                 │                          │
                 │ Metrics / trends         │
                 │ Audit &amp;amp; security events  │
                 │ Business-critical events │
                 └──────────────────────────┘
                              ▲
                              │
                       retain longer
                              │
                 ┌──────────────────────────┐
                 │     Short-lived data     │
                 │                          │
                 │ Application logs         │
                 │ Traces                   │
                 │ Debug information        │
                 └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initial policy
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Telemetry&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enough for normal debugging and incident investigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traces&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primarily useful for investigating recent requests/errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14 days initially&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sufficient during beta/MVP; increase later for long-term trends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug logs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;As short as practical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High volume and usually low long-term value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security/audit events&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Separate policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May require significantly longer retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business-critical events&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Separate storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Should not depend on observability retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The goal is &lt;strong&gt;not&lt;/strong&gt; to keep everything forever. The goal is to retain the information for as long as it is useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should Retention Increase
&lt;/h2&gt;

&lt;p&gt;Increase retention when we have a concrete reason, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bug occurs less frequently than the current retention window.&lt;/li&gt;
&lt;li&gt;We need to investigate incidents discovered weeks later.&lt;/li&gt;
&lt;li&gt;We need historical performance/capacity trends.&lt;/li&gt;
&lt;li&gt;Security or compliance requirements require longer retention.&lt;/li&gt;
&lt;li&gt;The application becomes business-critical and historical investigation becomes important.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Beta:
  Logs/Traces ─────────────── 14 days

Growing production:
  Logs/Traces ─────────────── 30–90 days
  Metrics ─────────────────── 6–13+ months
  Audit/Security ──────────── separate policy

Compliance/security:
  Audit data ──────────────── potentially years
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; automatically increase raw-log retention just because the application grows. Long-term trends are often better represented by metrics, while important audit/business events can be archived separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid Vendor Lock-in
&lt;/h2&gt;

&lt;p&gt;The application should &lt;strong&gt;never depend directly on a vendor-specific observability SDK or API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    │
    │ OpenTelemetry
    ▼
OTel Collector
    │
    ├──────────► Grafana Cloud
    │
    ├──────────► Honeycomb
    │
    └──────────► Other OTel backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenTelemetry Collector is specifically designed to receive, process, and export telemetry to one or more backends. This means changing providers should primarily be a &lt;strong&gt;Collector configuration/deployment change&lt;/strong&gt;, rather than an application rewrite. Use &lt;strong&gt;OTLP&lt;/strong&gt;, the standard OpenTelemetry protocol, for the application → Collector boundary.&lt;/p&gt;

&lt;p&gt;Also avoid making provider-specific dashboards, alerts, queries, and metadata a critical part of the application architecture until there is a reason to commit to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenTelemetry?
&lt;/h2&gt;

&lt;p&gt;OTel is a &lt;strong&gt;vendor-neutral, open-source observability framework&lt;/strong&gt; for generating, collecting, and exporting logs, metrics, and traces. It is supported by a broad ecosystem of observability vendors.&lt;/p&gt;

&lt;p&gt;Adopting OTel gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vendor portability.&lt;/li&gt;
&lt;li&gt;Consistent telemetry semantics.&lt;/li&gt;
&lt;li&gt;Logs ↔ traces ↔ metrics correlation.&lt;/li&gt;
&lt;li&gt;Centralized sampling/filtering.&lt;/li&gt;
&lt;li&gt;The ability to change observability backends later.&lt;/li&gt;
&lt;li&gt;The option to send telemetry to multiple backends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Principle:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instrument once with OpenTelemetry. Choose the observability backend independently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Read More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;OpenTelemetry documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/collector/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;OpenTelemetry Collector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/specs/otlp/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;OTLP specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/products/cloud/free-tier/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Grafana Cloud Free&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>observability</category>
      <category>opentelemetry</category>
      <category>cloudinfrastructure</category>
      <category>devops</category>
    </item>
    <item>
      <title>Some title</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:14:44 +0000</pubDate>
      <link>https://dev.to/kasir-barati/some-title-515b</link>
      <guid>https://dev.to/kasir-barati/some-title-515b</guid>
      <description>&lt;p&gt;We released XYZ feature&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.toNone"&gt;Watch on YouTube&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kasir-barati.github.io/" rel="noopener noreferrer"&gt;My Profile&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>some</category>
      <category>hashtags</category>
    </item>
    <item>
      <title>A Database Design Mistake Which Shows itself in the API</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Mon, 07 Sep 2026 22:13:09 +0000</pubDate>
      <link>https://dev.to/kasir-barati/a-database-design-mistake-which-shows-itself-in-the-api-31nh</link>
      <guid>https://dev.to/kasir-barati/a-database-design-mistake-which-shows-itself-in-the-api-31nh</guid>
      <description>&lt;p&gt;Imagine you have a &lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/prisma/schema.prisma#L66-L67" rel="noopener noreferrer"&gt;&lt;code&gt;ChapterContent&lt;/code&gt;&lt;/a&gt; table in PostgreSQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model ChapterContent {
  // ...
  content            String  @db.Text
  ttsFriendlyContent String? @map("tts_friendly_content") @db.Text

  contentHash String  @map("content_hash")
  ttsHash     String? @map("tts_hash")
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I initially though what an ingenious thing I did, two &lt;code&gt;String&lt;/code&gt; columns, sitting next to each other, where the &lt;code&gt;ttsFriendlyContent&lt;/code&gt; only makes sense as a function of &lt;code&gt;content&lt;/code&gt; and that invariant lives entirely in application code, spread across codebase, instead of being enforced by the data model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;

&lt;p&gt;When I say "function" I mean it in the mathematical sense: &lt;code&gt;ttsFriendlyContent = f(content)&lt;/code&gt;, its value should be entirely determined by content (plus a bunch of rules we have for it, e.g. not using ~ for elongations). Given the same content, there's only one correct ttsFriendlyContent; it's not meant to be an independent value someone can set arbitrarily.&lt;/p&gt;

&lt;p&gt;That's why storing it as its own free-standing column (rather than as a computed/derived artifact) is a design smell, the schema lets you set it to anything, even though semantically it should only ever be "whatever content maps to". And another relevant issue is maintaining it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Invariant Nobody can See
&lt;/h2&gt;

&lt;p&gt;The rule I actually need enforced is simple to state and hard to find:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You must generate &lt;code&gt;ttsFriendlyContent&lt;/code&gt; from &lt;code&gt;content&lt;/code&gt; before you can generate narration audio from &lt;code&gt;ttsFriendlyContent&lt;/code&gt;. You cannot set &lt;code&gt;ttsFriendlyContent&lt;/code&gt; without &lt;code&gt;content&lt;/code&gt; already existing. If &lt;code&gt;content&lt;/code&gt; changes, &lt;code&gt;ttsFriendlyContent&lt;/code&gt; becomes stale and narration built from it is now wrong too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;None of that is expressed by the schema. It's enforced (partially) by scattered checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/src/modules/novel/services/chapter.service.ts#L42-L63" rel="noopener noreferrer"&gt;&lt;code&gt;ChapterService#updateContent#updateContent&lt;/code&gt;&lt;/a&gt; accepts &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;ttsFriendlyContent&lt;/code&gt; as &lt;strong&gt;two independent, client-supplied strings&lt;/strong&gt; in the same call, with a &lt;code&gt;TODO&lt;/code&gt; admitting the obvious hole 😅:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// TODO: validate the ttsFriendlyContent make sense (the content should match the TTS-friendly version)!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    actor Client
    participant API as GraphQL API
    participant Service as ChapterService#updateContent
    participant Repo as ChapterContentRepository

    Client-&amp;gt;&amp;gt;API: mutation (cnt: "He", ttsFriendlyCnt: "asd")
    Note over Client: Cannot use equality checks!

    API-&amp;gt;&amp;gt;Service: updateContent(chapterId, content, ttsFriendlyContent)
    Note over Service: Already too late to validate

    Service-&amp;gt;&amp;gt;Repo: upsertByChapterId(chapterId, content, ttsFriendlyContent)
    Note over Repo: Does NOT validates ttsFriendlyContent either

    Repo--&amp;gt;&amp;gt;Service: stored ✅ (silently wrong)
    Service--&amp;gt;&amp;gt;API: chapter
    API--&amp;gt;&amp;gt;Client: 😵 200 OK&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;You ask why we &lt;strong&gt;cannot use equality checks?&lt;/strong&gt; Because one is suppose to the the text user sees while the other is a a machine-generated version of the content.&lt;/p&gt;

&lt;p&gt;And just in case you are wondering why I wrote "too late to validate", I meant that validation should have been done when we received the request (DTO layer).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Right now there's exactly one door into &lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/prisma/schema.prisma#L64" rel="noopener noreferrer"&gt;updating &lt;code&gt;ChapterContent&lt;/code&gt;'s &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;ttsFirendlyContent&lt;/code&gt; fields&lt;/a&gt;, the good thing is that client must send both fields every time, so the "set ttsFriendlyContent before content exists" scenario can't happen. It only becomes a real ordering problem &lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/src/modules/novel/resolvers/chapter.resolver.ts#L80-L100" rel="noopener noreferrer"&gt;if we add &lt;code&gt;createChapter&lt;/code&gt; mutation&lt;/a&gt;: then a chapter could exist with &lt;code&gt;content&lt;/code&gt; but no &lt;code&gt;ttsFriendlyContent&lt;/code&gt;, and a later &lt;code&gt;updateContent&lt;/code&gt; call would need to know whether it's being asked to set &lt;code&gt;ttsFriendlyContent&lt;/code&gt; for the first time, edit &lt;code&gt;content&lt;/code&gt;, or both! Drawing a line between them is costly and very much error-prone.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    actor Client
    participant API as GraphQL API
    participant Svc as ChapterService

    Note over Client,Svc: Today - updateContent is the only mutation touching chapter_contents table, both args required together
    Client-&amp;gt;&amp;gt;API: updateContent(id, content, ttsFriendlyContent)
    API-&amp;gt;&amp;gt;Svc: writes both fields in one call - no way to send just one

    Note over Client,Svc: Hypothetical - if createChapter ships as its own mutation
    Client-&amp;gt;&amp;gt;API: createChapter(content)
    API-&amp;gt;&amp;gt;Svc: chapter row created, ttsFriendlyContent still null
    Client-&amp;gt;&amp;gt;API: updateContent(id, content, ttsFriendlyContent)
    Note over Svc: this second call could also silently change content again&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/src/modules/novel/repositories/prisma-chapter-content.repository.ts#L45-L70" rel="noopener noreferrer"&gt;&lt;code&gt;PrismaChapterContentRepository#upsertByChapterId&lt;/code&gt;&lt;/a&gt; recomputes &lt;code&gt;contentHash&lt;/code&gt; from &lt;code&gt;content&lt;/code&gt; on every write, but &lt;strong&gt;never touches &lt;code&gt;ttsHash&lt;/code&gt;&lt;/strong&gt;. The column exists, it's indexed (&lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/prisma/schema.prisma#L72" rel="noopener noreferrer"&gt;&lt;code&gt;schema.prisma#L89&lt;/code&gt;&lt;/a&gt;), it's read back in the repository mapper, but nothing ever writes or compares it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a staleness-detection mechanism that was designed and I did not get to finish it, but the point is that the two-fields-as-siblings shape doesn't naturally produce a "check if stale" step; you have to bolt it on and remember to call it everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel/blob/7ef4d9f25bbeac9e3226b9009f76d74505f4147b/apps/backend/src/modules/novel/services/chapter-narration.service.ts#L66-L171" rel="noopener noreferrer"&gt;&lt;code&gt;ChapterNarrationService#startGeneration(chapterId, forceRegenerate)&lt;/code&gt;&lt;/a&gt; layers on Redis locking, in-flight HTTP-request cancellation, and a double-checked-locking re-read of the DB, all to answer one question safely: &lt;em&gt;is the narration we're about to build actually derived from the current content?&lt;/em&gt; This is a lot of concurrency machinery compensating for the fact that the data model can't say "this narration is derived from commit X of this content" on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is Hard to Maintain
&lt;/h2&gt;

&lt;p&gt;None of these four pieces is complicated in isolation. The problem is that the actual business rule, &lt;em&gt;content → tts-friendly text → narration, each derived and invalidated by the previous step&lt;/em&gt; is nowhere written down as a single flow and they are scattered across multiple mutations. Miss any one piece when adding a new mutation path or changing one, and you get either a rejected valid request or worse narration audio generated from stale text, with no error at all.&lt;/p&gt;

&lt;p&gt;Here's the actual authoring workflow that demonstrates the crux of the matter:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["author writes content"] --&amp;gt; B["updateContent - content stored, contentHash recomputed"]
    B --&amp;gt; C["generateTtsFriendlyText - preview only, not persisted"]
    C --&amp;gt; D["updateContent again - ttsFriendlyContent persisted"]
    D --&amp;gt; E["startGeneration - narration built from ttsFriendlyContent"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The flow is of mutations is far apart in time and they all work on the same &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;ttsFriendlyContent&lt;/code&gt; fields. It is very easy to break the flow and introduce bugs when developing new features, fixing a bug, or refactoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Potential Fixes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The textbook answer here is &lt;a href="https://www.postgresql.org/docs/current/ddl-generated-columns.html" rel="noopener noreferrer"&gt;derived/generated columns&lt;/a&gt; or a version/lineage table which stores &lt;code&gt;content&lt;/code&gt;, and &lt;code&gt;content_version&lt;/code&gt;, then the &lt;code&gt;tts_friendly_content&lt;/code&gt; (and &lt;code&gt;narration&lt;/code&gt;) fields reference the exact version they were derived from, so staleness is a join/comparison instead of a hash which is maintained at the application level (imagine we had implemented it and use the &lt;code&gt;ttsHash&lt;/code&gt;) prone to human-errors or application-level bugs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same shape as &lt;a href="https://martinfowler.com/bliki/CQRS.html" rel="noopener noreferrer"&gt;event sourcing/CQRS read-model invalidation&lt;/a&gt; or a plain &lt;a href="https://en.wikipedia.org/wiki/Content-addressable_storage" rel="noopener noreferrer"&gt;content-addressable cache&lt;/a&gt;: never overwrite a derived artifact in place, key it by the hash of its source, and treat a hash mismatch as "recompute," not as a manual flow someone else has to remember to do.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop maintaining &lt;code&gt;ttsFriendlyContent&lt;/code&gt; as hand-sanitized text all together.&lt;/strong&gt; Instead of a regex pass + LLM normalization step that produces a &lt;em&gt;separate stored string&lt;/em&gt; someone has to keep in sync, I'm going to hand the raw &lt;code&gt;content&lt;/code&gt; straight to &lt;a href="https://github.com/QwenLM/Qwen3-TTS" rel="noopener noreferrer"&gt;Qwen3-TTS&lt;/a&gt; with instructions in the prompt for how to handle markdown, elongated words, ALL-CAPS, bracketed sound cues, etc. Instead of normalizing the content first at generation time, not persistence time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This doesn't fix the schema design mistake in a principled way. I am sidestepping it by deleting the second field that had to stay in sync. There's only one source string now (&lt;code&gt;content&lt;/code&gt;), and "TTS-friendly" becomes an instruction send to &lt;a href="https://kasir-barati.github.io/smart-novel-beatrice/v3.0.0/#mutation-generateAudio" rel="noopener noreferrer"&gt;Beatrice's &lt;code&gt;generateAudio&lt;/code&gt; mutation&lt;/a&gt;. The instructions can be as rigid as a hardcoded string we can embed in the mutation itself, or as flexible as enabling authors to customize how Beatrice handles their content when it wanted to generate audio. Fewer fields, fewer hashes to forget to update, fewer maintenance taxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long story short I decided to go with the second option.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proper fix, content-addressable derived rows with explicit lineage is still the right answer if a project needs auditable, cacheable, multi-version derived content. &lt;strong&gt;For a single-author, single-version chapter model, removing the second field is cheaper than making the sync mechanism correct&lt;/strong&gt;. Also keep in mind here we are talking about novels and chapters. So it should not be that big of a deal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;

&lt;p&gt;I had a nasty experience with dev.to itself when I was using their platform to update my posts and then they just mixed it and my post was half old content and half new content. That was a disaster and why I decided to move to a more stable, local, and disciplined approach which was using a Version Control System. Currently I write my posts locally on my lovely Zed IDE and push them to GitHub, then there is a GitHub Actions which automatically publishes the posts to dev.to or schedule it.&lt;/p&gt;

&lt;p&gt;I guess I am very much fond of the same pattern for smart-novel. AKA authors can write on their own local machines and push them to some sort of VSC. But the only issues is that they are most of the times not into tech. But I believe with LLMs and right amount of tooling I can make it easier for them to write their novels. I am thinking of having a MCP server which would exposes a couple of skills helping them register at GitHub, create a repository, add secrets and necessary env vars. So the nitty-gritty details are hidden away from the non-tech authors.&lt;/p&gt;

&lt;p&gt;And of course they can still use the beautiful UI to update their posts. But I guess this demands a bit more brainstorming since I already had tried a couple of times to update my dev.to posts in their UI since it was a single line change. But then I remembered that would corrupt my post in my GitHub repository since now I have a change which does not exists in the VCS. If by any chance I update the markdown file in my repo and push it, it will override the changes I made in the dev.to UI.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;GitHub Repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel" rel="noopener noreferrer"&gt;smart-novel is a novel writing platform which addresses frustrations I had when visiting and reading other website to read novel online&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kasir-barati/smart-novel-beatrice" rel="noopener noreferrer"&gt;Beatrice is the companion LLM-powered app for smart-novel&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>databasedesign</category>
      <category>postgres</category>
      <category>llm</category>
      <category>tts</category>
    </item>
    <item>
      <title>Node Sizing, Resource Alarms</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sun, 06 Sep 2026 17:45:11 +0000</pubDate>
      <link>https://dev.to/kasir-barati/node-sizing-resource-alarms-2l7e</link>
      <guid>https://dev.to/kasir-barati/node-sizing-resource-alarms-2l7e</guid>
      <description>&lt;p&gt;Maybe you also are curious as to why we picked &lt;code&gt;t3.medium&lt;/code&gt; for the cluster and asked the obvious question: &lt;strong&gt;is this cluster actually sized for what we're running on it, and how would we know if it stopped being sized for it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is &lt;code&gt;t3.medium&lt;/code&gt; (&lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/variables.tf#L41" rel="noopener noreferrer"&gt;the instance type &lt;code&gt;variables.tf&lt;/code&gt; defaults to&lt;/a&gt;) actually enough for 11 pods across 2 nodes?&lt;/p&gt;

&lt;p&gt;How would you get &lt;em&gt;paged&lt;/em&gt; the moment you're provisioned cluster runs out of resources instead of finding out from a &lt;code&gt;CrashLoopBackOff&lt;/code&gt; at 2am?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;

&lt;p&gt;JFYI, you might see &lt;code&gt;ContainerCreating&lt;/code&gt; when running &lt;code&gt;kubectl get pods&lt;/code&gt;, it basically means the kubelet has a node assigned and is doing setup work before the container process starts, and the single biggest contributor to that phase, on a brand new node, is &lt;strong&gt;pulling the image&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Is &lt;code&gt;t3.medium&lt;/code&gt; Actually Enough for These 11 Pods
&lt;/h2&gt;

&lt;p&gt;Short answer: yes, comfortably so for the current setup 😉.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's actually running:&lt;/strong&gt; &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/voting-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;voting&lt;/code&gt; (3 replicas)&lt;/a&gt;, &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/result-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;result-deployment&lt;/code&gt; (3)&lt;/a&gt;, &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/worker-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;worker-deployment&lt;/code&gt; (3)&lt;/a&gt;, plus the standalone &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/db-pod.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;db&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/redis-pod.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/a&gt;, 11 application pods in total, against &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/variables.tf#L44-L60" rel="noopener noreferrer"&gt;&lt;code&gt;node_desired_size = 2&lt;/code&gt;, &lt;code&gt;node_min_size = 1&lt;/code&gt;, &lt;code&gt;node_max_size = 3&lt;/code&gt; Kubernetes worker nodes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is important to understand that pods with the current setup are allocated per node, not CPU/RAM.&lt;/strong&gt; EKS caps pods per node by ENI/IP capacity, not raw compute, because of how the VPC CNI assigns a real VPC IP to every pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max pods = (number of ENIs) × (IPv4 addresses per ENI − 1) + 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;t3.medium&lt;/code&gt; that's 3 ENIs × (6 − 1) + 2 = &lt;strong&gt;17 pods per node&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Subtract system pods that live on every node (&lt;code&gt;kube-proxy&lt;/code&gt;, &lt;code&gt;aws-node&lt;/code&gt;/vpc-cni, and &lt;code&gt;coredns&lt;/code&gt; typically spread across 1-2 nodes), let's say we have 3-5 slots already reserved. With that in mind we roughly have 12-14 app-pod slots per node, ~24-28 across the 2 desired nodes. 11 pods fits with room to spare. This ceiling might bite quietly by running out of slots the moment you scale replicas up (&lt;code&gt;kubectl scale --replicas=10 deployment/voting-deployment&lt;/code&gt; would eat most of that headroom by itself, does not matter if it was auto-scaled or not).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other issue is that I did NOT declare &lt;code&gt;resources.requests&lt;/code&gt;/&lt;code&gt;limits&lt;/code&gt;.&lt;/strong&gt; Check &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/voting-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;voting-deployment.yaml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/result-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;result-deployment.yaml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/worker-deployment.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;worker-deployment.yaml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/db-pod.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;db-pod.yaml&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/redis-pod.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;redis-pod.yaml&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;None of them tell Kubernetes its CPU or memory requirements! That means the scheduler treats every pod as needing &lt;strong&gt;zero&lt;/strong&gt; resources for placement purposes, so it will keep packing pods onto a node with no real headroom check at all. That means we won't be seeing a clean &lt;code&gt;0/2 nodes are available: Insufficient memory&lt;/code&gt; message when you're actually out of capacity.&lt;/p&gt;

&lt;p&gt;Instead, actual memory usage creeps up under real traffic until the kubelet hits an eviction threshold and starts killing pods with &lt;code&gt;OOMKilled&lt;/code&gt; or &lt;code&gt;Evicted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;t3.medium&lt;/code&gt; = 2 vCPU / 4 GiB RAM&lt;/strong&gt;. So we have the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Replicas&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL 17 Alpine&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis 7 Alpine&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;example-voting-app-result&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NodeJS result web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;example-voting-app-vote&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Python voting web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;example-voting-app-worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;.NET worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total application containers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And you &lt;strong&gt;do not get to treat those 4 GiB and 2 CPUs as application capacity&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│ EC2 t3.medium                 │
│                               │
│ 2 vCPU                        │
│ 4 GiB RAM                     │
│                               │
│ ┌───────────────────────────┐ │
│ │ Linux / kernel            │ │
│ │ kubelet                   │ │
│ │ container runtime         │ │
│ │ AWS/EKS components        │ │
│ └───────────────────────────┘ │
│                               │
│ ┌───────────────────────────┐ │
│ │ Your Pods                 │ │
│ │                           │ │
│ │ postgres                  │ │
│ │ redis                     │ │
│ │ result ×3                 │ │
│ │ vote ×3                   │ │
│ │ worker ×3                 │ │
│ └───────────────────────────┘ │
└───────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources" rel="noopener noreferrer"&gt;Kubernetes explicitly distinguishes &lt;strong&gt;node capacity&lt;/strong&gt; from &lt;strong&gt;node allocatable&lt;/strong&gt;&lt;/a&gt;. Allocatable is the amount available to Pods after accounting for resources reserved for Kubernetes/OS components. &lt;a href="https://docs.aws.amazon.com/eks/latest/eksctl/customizing-the-kubelet.html" rel="noopener noreferrer"&gt;AWS likewise documents reserving resources for kubelet/system daemons&lt;/a&gt; because otherwise resource starvation can cause the node to become unhealthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another factor which I deliberately did not touch is the fact that &lt;code&gt;max_size = 3&lt;/code&gt; doesn't mean autoscaling.&lt;/strong&gt; &lt;code&gt;node_max_size&lt;/code&gt; only sets the ceiling on the &lt;em&gt;managed node group's&lt;/em&gt; underlying ASG. It's a permission slip, not a trigger. Nothing in our Terraform config actually watches for unschedulable pods and raises the desired count toward that ceiling. Without &lt;a href="https://github.com/kubernetes/autoscaler" rel="noopener noreferrer"&gt;Cluster Autoscaler&lt;/a&gt; or &lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;Karpenter&lt;/a&gt; installed in the cluster, you can be pod-starved with two idle nodes sitting at &lt;code&gt;desired_size = 2&lt;/code&gt; and a third one it's &lt;em&gt;allowed&lt;/em&gt; to create but has no reason to 😅.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do NOT Calculate it Like this
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Postgres needs 512 MB, Redis needs 100 MB, each app needs 100 MB, therefore we're good.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not gonna work!&lt;/p&gt;

&lt;p&gt;Also it worth mentioning that currently we need to wire up alerting to detect and respond to resource pressures. The signal we need is &lt;strong&gt;unschedulable pods&lt;/strong&gt; (&lt;code&gt;Pending&lt;/code&gt; with a &lt;code&gt;FailedScheduling&lt;/code&gt; event citing insufficient CPU/memory/pod-IPs) and &lt;strong&gt;node resource reservation&lt;/strong&gt; approaching 100%. AWS's own path for this is &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights.html" rel="noopener noreferrer"&gt;CloudWatch Container Insights&lt;/a&gt;, which we can enable on the cluster. It publishes metrics like &lt;code&gt;node_cpu_reserved_capacity&lt;/code&gt;, &lt;code&gt;node_memory_reserved_capacity&lt;/code&gt;, and &lt;code&gt;cluster_failed_node_count&lt;/code&gt; into CloudWatch, on top of which you set a plain CloudWatch Alarm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"node_memory_pressure"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.cluster_name}-node-memory-reserved-high"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ContainerInsights"&lt;/span&gt;
  &lt;span class="nx"&gt;metric_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"node_memory_utilization"&lt;/span&gt;
  &lt;span class="nx"&gt;dimensions&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ClusterName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;statistic&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Average"&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
  &lt;span class="nx"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;comparison_operator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_actions&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container Insights will run an agent/Fluent Bit DaemonSet, there is a cheaper and more direct alternative: &lt;strong&gt;alerting on &lt;code&gt;kube_pod_status_phase{phase="Pending"}&lt;/code&gt;&lt;/strong&gt; via &lt;code&gt;kube-state-metrics&lt;/code&gt; + &lt;a href="https://prometheus.io/docs/alerting/latest/alertmanager" rel="noopener noreferrer"&gt;Prometheus + Alertmanager&lt;/a&gt; or we can go for the AWS-managed equivalent, Amazon Managed Service for Prometheus. That metric goes non-zero the instant a pod can't be placed.&lt;/p&gt;

&lt;p&gt;We also must set &lt;code&gt;resources.requests&lt;/code&gt;/&lt;code&gt;limits&lt;/code&gt; in the manifests. Without them, Kubernetes has nothing to compare "current usage" against, so there's no signal to alarm on beyond raw node metrics. With requests set, you get two things for free: the scheduler stops overpacking nodes, and you can receive alarms on &lt;code&gt;kube_pod_container_resource_limits&lt;/code&gt;, i.e. "this pod is at 90% of its memory limit" for example well before it gets &lt;code&gt;OOMKilled&lt;/code&gt;. That's a config change to the &lt;code&gt;*-deployment.yaml&lt;/code&gt; Kubernetes manifest files.&lt;/p&gt;

&lt;p&gt;These are 4 different concepts to estimate how much CPU/RAM a pod will be needing:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["📦 IMAGE SIZE: How much disk space the image consumes? postgres:17-alpine Image = 118.16 MB"]

    B["📊 ACTUAL RUNTIME USAGE: What the process actually consumes while running (workload shape)? E.g. Postgres needs at least 1GB of RAM, 1 CPU core"]

    C["⎈ What Kubernetes reserves for scheduling purposes."]

    D["🚧 KUBERNETES LIMIT: Maximum resource the container is allowed to consume."]

    A --&amp;gt;|"Image artifact != runtime resources"| B
    B --&amp;gt;|"Observe &amp;amp; measure actual behavior"| C
    C --&amp;gt;|"Set an upper boundary for runtime"| D&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Some good to know notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workload shape, not habit.&lt;/strong&gt; E.g. this app is CPU-light, and memory-modest, but I/O intensive. A CPU-bound workload (video transcoding, heavy compute) wants compute-optimized (&lt;code&gt;c-family&lt;/code&gt;); a memory-heavy workload (large caches, in-memory analytics) wants &lt;code&gt;r-family&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A production node group under steady load usually wants &lt;code&gt;m5&lt;/code&gt;/&lt;code&gt;m6i&lt;/code&gt; (fixed, non-burstable performance) instead, specifically to avoid throttling under sustained load (&lt;code&gt;t3.*&lt;/code&gt; throttles under sustained load).&lt;/li&gt;
&lt;li&gt;If you're running many small pods (a common pattern with microservices), you can be pod-slot constrained well before you're resource-constrained. This is AWS-specific (the VPC CNI's IP-per-ENI model), not a general Kubernetes fact, and it's easy to miss because &lt;code&gt;kubectl describe node&lt;/code&gt; reports it as &lt;code&gt;pods: 17&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;ARM64 Graviton instances are typically 10-20% cheaper for equivalent performance on workloads that don't depend on x86-only binaries. So if you can swap &lt;code&gt;ami_type = "AL2023_x86_64_STANDARD"&lt;/code&gt; for &lt;code&gt;AL2023_ARM_64_STANDARD&lt;/code&gt; and the containers will be able to run then you can save on costs.&lt;/li&gt;
&lt;li&gt;Add something like this to your Kubernetes manifest files:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Once traffic is real, you can collect metrics from &lt;a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/" rel="noopener noreferrer"&gt;Kubelet &lt;code&gt;/metrics/resource&lt;/code&gt; endpoint directly&lt;/a&gt;. Or &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ContainerInsights.html" rel="noopener noreferrer"&gt;Container Insights&lt;/a&gt; tells you actual CPU/memory usage versus allocatable. Resize the node group only after you have that data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine you measure a container's RAM usage 100 times (same is true for CPU):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;50, 51, 52, 53, ... 98, 100, 120 MiB&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then you need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;median&lt;/strong&gt;: roughly the point where 50% of measurements are below it and 50% are above it. So if median RAM = 70 MiB, the container typically uses around 70 MiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p95&lt;/strong&gt;: 95% of your measurements are at or below this value. If it is 120 MiB, then 95% of your measurements are below 120 MiB. This is the answer to the &lt;strong&gt;"How much does it use during almost all normal situations?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99&lt;/strong&gt;: is exactly the same as p95. And in layman's terms: &lt;strong&gt;"How high does usage get during almost all situations, including occasional spikes?"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;maximum&lt;/strong&gt;: The single highest measurement you observed. But be careful about this one, you might have measurements like this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;70
72
68
75
71
69
350  ← weird spike, investigate what happened at that time!
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;behavior under load&lt;/strong&gt;: What happens to CPU, memory, latency, errors, etc. when you give the application more work? For example you could have:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users/requests       CPU       RAM       Response time
──────────────────────────────────────────────────────
10                    10%      80 MiB       20 ms
100                   25%      90 MiB       25 ms
500                   60%     120 MiB       40 ms
1000                  95%     180 MiB      200 ms
2000                 100%     300 MiB     2000 ms  ← struggling
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simple mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Median  → "What's typical?"
  P95     → "What happens most of the time, including busy periods?"
  P99     → "What happens during nearly all but the rarest spikes?"
  Maximum → "What's the biggest thing I actually observed?"
  Load    → "What happens when I give it more work?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  So Why &lt;code&gt;t3.medium&lt;/code&gt; &amp;amp; Takeaway
&lt;/h2&gt;

&lt;p&gt;Honestly I just picked something that we can get over with in this tutorial, not a sized decision based on the workload 😅.&lt;/p&gt;

&lt;p&gt;What's actually missing isn't node capacity, it's &lt;strong&gt;visibility&lt;/strong&gt;: no &lt;code&gt;resources.requests&lt;/code&gt;/&lt;code&gt;limits&lt;/code&gt; on any pod, and no alerting layer watching either node pressure or pod-level usage. So before adopting similar setup for production traffic you must first add visibility.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requests aren't actual usage.&lt;/strong&gt; This distinction causes enormous confusion in Kubernetes, so if you have:&lt;/p&gt;


&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1Gi"&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;And &lt;strong&gt;the application actually uses only 500MiB&lt;/strong&gt;, the node will still be overprovisioned. You can visualize it like this:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node:
    allocatable RAM = 2Gi

Pods:
    requests = 1Gi

Actual usage:
    500MiB
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This is why I said you &lt;strong&gt;MUST add visibility to your infrastructure&lt;/strong&gt; first.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>eks</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Setup Infra, Deploy the App, Verify, and Tear It All Down</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sun, 06 Sep 2026 11:08:24 +0000</pubDate>
      <link>https://dev.to/kasir-barati/setup-infra-deploy-the-app-verify-and-tear-it-all-down-12ik</link>
      <guid>https://dev.to/kasir-barati/setup-infra-deploy-the-app-verify-and-tear-it-all-down-12ik</guid>
      <description>&lt;p&gt;I hope now you have a fundamental grasp on the what Terraform will do. This part is about setting up the infrastructure, deploying the app, verifying it works, and tearing it all down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;Install Terraform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;Install AWS CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/tools/#kubectl" rel="noopener noreferrer"&gt;Install kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kasir-barati/looking-at-what-we-are-building-1flf"&gt;Create an IAM user and access keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;terraform init&lt;/code&gt; &amp;amp; &lt;code&gt;terraform plan&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:kasir-barati/docker.git
&lt;span class="nb"&gt;cd &lt;/span&gt;k8s/voting-microservice-architecture/deployment/terraform
&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars   &lt;span class="c"&gt;# optional: edit values here&lt;/span&gt;

terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; downloads the &lt;code&gt;aws&lt;/code&gt; provider and the &lt;code&gt;vpc&lt;/code&gt;/&lt;code&gt;eks&lt;/code&gt; modules into a local &lt;code&gt;.terraform/&lt;/code&gt; folder, and writes/checks &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; (pins exact provider versions. This file &lt;em&gt;should&lt;/em&gt; be committed to Git, unlike state (in the repo it is already present, but I just wanted to be clear about it).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get a long list ending in something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 47 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;47-ish resources for "one VPC and one EKS cluster" sounds like a lot until you remember a module expands into subnets × 2 AZs, route tables, IAM roles and policy attachments, the node group's launch template, security groups, etc. Most of the times you wanna skim through it. BTW at this stage nothing has touched AWS yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;terraform apply&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Same plan, followed by a &lt;code&gt;yes&lt;/code&gt; confirmation prompt. This step genuinely takes a while, &lt;strong&gt;EKS control planes take roughly 10-15 minutes to provision&lt;/strong&gt;, plus a few more minutes for the node group's EC2 instances to launch and join. This is normal; it's AWS provisioning managed infrastructure, not a Terraform slowness so feel free to grab a coffee.&lt;/p&gt;

&lt;p&gt;When it finishes, you'll see the outputs from &lt;code&gt;outputs.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outputs:

cluster_endpoint = "https://ABCDEF1234167890.gr7.eu-central-1.eks.amazonaws.com"
cluster_name = "voting-app-cluster"
configure_kubectl = "aws eks update-kubeconfig --region eu-central-1 --name voting-app-cluster"
region = "eu-central-1"
vpc_id = "vpc-0123456789abcdef0"
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Point &lt;code&gt;kubectl&lt;/code&gt; at the new Cluster
&lt;/h2&gt;

&lt;p&gt;Run the exact command from &lt;code&gt;configure_kubectl&lt;/code&gt; above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws eks update-kubeconfig &lt;span class="nt"&gt;--region&lt;/span&gt; eu-central-1 &lt;span class="nt"&gt;--name&lt;/span&gt; voting-app-cluster
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                       STATUS   ROLES    AGE   VERSION
ip-10-0-0-123.ec2.internal                 Ready    &amp;lt;none&amp;gt;   2m    v1.33.x-eks-...
ip-10-0-1-45.ec2.internal                  Ready    &amp;lt;none&amp;gt;   2m    v1.33.x-eks-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;Ready&lt;/code&gt; nodes, one per AZ, this is the point where a real EKS cluster exists and you could deploy literally anything to it, not just this app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy the Voting App
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;k8s/voting-microservice-architecture/deployment
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies the plain Kubernetes manifests; Postgres, Redis, and the replicated vote/result/worker app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a Public URL
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;voting-service&lt;/code&gt; and &lt;code&gt;result-service&lt;/code&gt; ship as &lt;code&gt;type: LoadBalancer&lt;/code&gt;. On minikube, &lt;code&gt;type: LoadBalancer&lt;/code&gt; is a superset of &lt;code&gt;NodePort&lt;/code&gt; so &lt;code&gt;minikube service list&lt;/code&gt; will show you the URLs of the voting and result apps. On EKS the worker nodes live in &lt;strong&gt;private&lt;/strong&gt; subnets with no public IP, so there'd be no node address to even try hitting without it. &lt;code&gt;kubectl apply -f .&lt;/code&gt; provisions the load balancers along with everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get svc voting-service result-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within 1-3 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME             TYPE           EXTERNAL-IP                                                     PORT(S)
voting-service   LoadBalancer   b1b2c3d4e5f6-1234567895.elb.eu-central-1.amazonaws.com               80:31234/TCP
result-service   LoadBalancer   q6e5d4c3b2a1-0987654322.elb.eu-central-1.amazonaws.com               80:31567/TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;EXTERNAL-IP&lt;/code&gt; column is an AWS-issued DNS hostname, the default behavior for a Network Load Balancer, it's &lt;strong&gt;free, requires no domain of your own, and is publicly resolvable the moment it appears.&lt;/strong&gt; Open them directly in a browser.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant You as Your browser
    participant DNS as AWS DNS (*.elb.amazonaws.com)
    participant NLB as Network Load Balancer
    participant Node as EKS worker node
    participant Pod as voting-app pod

    You-&amp;gt;&amp;gt;DNS: resolve a1b2c3...elb.eu-central-1.amazonaws.com
    DNS--&amp;gt;&amp;gt;You: NLB's public IP(s)
    You-&amp;gt;&amp;gt;NLB: HTTP GET :80
    NLB-&amp;gt;&amp;gt;Node: forward to a healthy node
    Node-&amp;gt;&amp;gt;Pod: kube-proxy routes to a voting pod
    Pod--&amp;gt;&amp;gt;You: response&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  6. Tear it All Down
&lt;/h2&gt;

&lt;p&gt;This is the part that trips people up, and it's worth understanding &lt;em&gt;why&lt;/em&gt;, not just memorizing the commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule: delete anything Kubernetes created before running &lt;code&gt;terraform destroy&lt;/code&gt;.&lt;/strong&gt; The Load Balancers were created by &lt;em&gt;Kubernetes'&lt;/em&gt; AWS integration reacting to &lt;code&gt;type: LoadBalancer&lt;/code&gt;, not by a Terraform &lt;code&gt;resource&lt;/code&gt; block. In other words, Terraform's state file has never heard of them.&lt;/p&gt;

&lt;p&gt;And if you remember &lt;code&gt;terraform destroy&lt;/code&gt; only deletes what's in state; it will delete the VPC's subnets and security groups regardless of an NLB still attached to them, which typically leaves that NLB orphaned or the terraform fails to delete the VPC. Have not verified it personally, but I suspect it does. Let me know in the comments if you know/experimented with it.&lt;/p&gt;

&lt;p&gt;That said &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/cleanup.tf" rel="noopener noreferrer"&gt;the &lt;code&gt;cleanup.tf&lt;/code&gt;&lt;/a&gt; runs something like a &lt;code&gt;kubectl delete -f .&lt;/code&gt; for you automatically as a destroy-time hook. So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Remove everything kubectl created, load balancers included&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;k8s/voting-microservice-architecture/deployment
kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 2. Now tear down the infrastructure Terraform actually owns&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;k8s/voting-microservice-architecture/deployment/terraform
terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;k8s/voting-microservice-architecture/deployment/terraform
terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["kubectl delete -f ."] --&amp;gt; B["NLBs actually deleted"]
    B --&amp;gt; C["terraform destroy"]
    C --&amp;gt; D["node group, EKS cluster, VPC, subnets, NAT gateway, IAM roles - all gone"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;code&gt;terraform destroy&lt;/code&gt; reads &lt;code&gt;terraform.tfstate&lt;/code&gt;, works out the &lt;em&gt;reverse&lt;/em&gt; dependency order and deletes each resource via the AWS API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying nothing's left
&lt;/h3&gt;

&lt;p&gt;Everything this project created is tagged &lt;code&gt;Project = voting-microservice-architecture&lt;/code&gt; (&lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/providers.tf#L4-L10" rel="noopener noreferrer"&gt;look at &lt;code&gt;providers.tf&lt;/code&gt;'s &lt;code&gt;default_tags&lt;/code&gt;&lt;/a&gt;). So go to the AWS Console → &lt;strong&gt;Resource Groups &amp;amp; Tag Editor&lt;/strong&gt; → search that tag in all regions and in all resource types.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;terraform destroy&lt;/code&gt; failed to delete all resources or your state file was corrupted, then you can find and delete them manually in AWS Console.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;That's the whole loop: infrastructure via Terraform, application via &lt;code&gt;kubectl&lt;/code&gt;, kept deliberately separate so each tool's domain of responsibility is clear.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>eks</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Walking Through the Terraform Config Files</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sun, 06 Sep 2026 08:59:33 +0000</pubDate>
      <link>https://dev.to/kasir-barati/walking-through-the-terraform-config-files-45mc</link>
      <guid>https://dev.to/kasir-barati/walking-through-the-terraform-config-files-45mc</guid>
      <description>&lt;p&gt;In this post I will walk through &lt;a href="https://github.com/kasir-barati/docker/tree/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform" rel="noopener noreferrer"&gt;Terraform config files&lt;/a&gt;. Here's the map before we zoom in file by file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;versions.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pins Terraform and provider versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;providers.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configures &lt;em&gt;which&lt;/em&gt; AWS account/region Terraform talks to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every configurable input, with sane defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vpc.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The network: VPC, subnets, NAT gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eks.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cluster: control plane, IAM roles, worker nodes, and the read-only access entry for the Console-viewer user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iam.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The human, Console-only IAM user used to browse pods/logs in the AWS Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read-only data sources (currently just the account ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values printed after &lt;code&gt;apply&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cleanup.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A destroy-time hook that removes Kubernetes-created LoadBalancers before the cluster/VPC teardown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform.tfvars.example&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Template for overriding defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Terraform doesn't care about file names or how many files you split things across. It reads &lt;em&gt;every&lt;/em&gt; &lt;code&gt;.tf&lt;/code&gt; file in the directory and merges them into one configuration. The split above is purely for us to have an easier time comprehending what we have.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/tree/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/versions.tf" rel="noopener noreferrer"&gt;&lt;code&gt;versions.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In this file we have &lt;code&gt;required_version&lt;/code&gt; which stops anyone from running this with a Terraform version old enough to not understand some syntax used here. &lt;code&gt;required_providers&lt;/code&gt; does the same for the AWS provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;

&lt;p&gt;&lt;code&gt;~&amp;gt; 6.0&lt;/code&gt; means "any &lt;code&gt;6.x&lt;/code&gt;, but not &lt;code&gt;7.0&lt;/code&gt;," so you get bugfixes automatically without silently picking up a breaking major version. This is what makes &lt;code&gt;terraform init&lt;/code&gt; reproducible on a different laptop or in CI a year from now.&lt;/p&gt;


&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["~&amp;gt; 1.5.0"] --&amp;gt; B["1.5.0 - Allowed"]
    A --&amp;gt; C["1.5.7 - Allowed"]
    A --&amp;gt; D["1.6.0 - NOT Allowed"]

    E["~&amp;gt; 1.5"] --&amp;gt; F["1.5.0 - Allowed"]
    E --&amp;gt; G["1.9.0 - Allowed"]
    E --&amp;gt; H["2.0.0 - NOT Allowed"]&lt;/code&gt;&lt;/pre&gt;


&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/tree/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/providers.tf" rel="noopener noreferrer"&gt;&lt;code&gt;providers.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Sets &lt;code&gt;region&lt;/code&gt;, read from a variable rather than being hardcoded, so switching regions is a one-line change in &lt;code&gt;terraform.tfvars&lt;/code&gt;, not a find-and-replace across every file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;

&lt;p&gt;&lt;code&gt;default_tags&lt;/code&gt; is applied to &lt;em&gt;every single resource&lt;/em&gt; the AWS provider creates in this run. It's how every VPC, EC2 instance, IAM role, etc. ends up tagged &lt;code&gt;Project = voting-microservice-architecture&lt;/code&gt; without repeating that tag block 20 times. Handy for finding everything this project owns in the AWS console later, and it's extremely useful when a module creates a resource which you did not specify in your own Terraform config files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/tree/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/variables.tf" rel="noopener noreferrer"&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Every variable follows this shape: a &lt;code&gt;type&lt;/code&gt; (string, number, bool, &lt;code&gt;list(string)&lt;/code&gt;, ...), a human-readable &lt;code&gt;description&lt;/code&gt;, and usually a &lt;code&gt;default&lt;/code&gt; so the project works out of the box. BTW this is variable precedence for overriding defaults:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    L1["Lowest, edit default in variables.tf"]

    L1 --&amp;gt; L1note["⚠️ Not recommended. Mixes config with input"]

    L1note -.overridden by.-&amp;gt; L2["Medium, terraform.tfvars. node_instance_types = ['t3.small']"]
    L2 --&amp;gt; L2note["✅ Recommended. This is why terraform.tfvars.example exists as a template"]

    L2note -.overridden by.-&amp;gt; L3["Highest, env var / CLI flag"]
    L3 --&amp;gt; L3a["export TF_VAR_node_instance_types='[t3.small]'"]
    L3 --&amp;gt; L3b["-var node_instance_types='[t3.small]'"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This project defines: &lt;code&gt;aws_region&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt;, &lt;code&gt;cluster_name&lt;/code&gt;, &lt;code&gt;kubernetes_version&lt;/code&gt;, &lt;code&gt;vpc_cidr&lt;/code&gt;, &lt;code&gt;azs&lt;/code&gt;, node sizing (&lt;code&gt;node_instance_types&lt;/code&gt;/&lt;code&gt;node_desired_size&lt;/code&gt;/&lt;code&gt;node_min_size&lt;/code&gt;/&lt;code&gt;node_max_size&lt;/code&gt;), &lt;code&gt;cluster_endpoint_public_access&lt;/code&gt;, and &lt;code&gt;console_viewer_username&lt;/code&gt;. Full list with descriptions can be seen in &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/variables.tf" rel="noopener noreferrer"&gt;the &lt;code&gt;variables.tf&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/vpc.tf" rel="noopener noreferrer"&gt;&lt;code&gt;vpc.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A few things worth explaining if you haven't used Terraform's function syntax before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;module "vpc" { source = "...", version = "..." }&lt;/code&gt;&lt;/strong&gt; is what we meant by "reuse someone's published &lt;code&gt;.tf&lt;/code&gt; files" in action. &lt;code&gt;source&lt;/code&gt; points at &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest" rel="noopener noreferrer"&gt;&lt;code&gt;terraform-aws-modules/vpc&lt;/code&gt; on the Terraform Registry&lt;/a&gt;; instead of us hand-writing every &lt;code&gt;aws_subnet&lt;/code&gt;, &lt;code&gt;aws_route_table&lt;/code&gt;, &lt;code&gt;aws_nat_gateway&lt;/code&gt;, etc. (which is a lot of boilerplate to get exactly right), we're using a battle-tested module that thousands of other projects already rely on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why CIDR exists:&lt;/strong&gt; every resource in your VPC (nodes, load balancers, NAT gateway) needs its own private IP address, and something has to decide up front which addresses are "ours" to hand out. Before your VPC can exist, you have to reserve a block of addresses for it, like requesting a range of extension numbers for an office phone system before anyone can be assigned one. CIDR notation (&lt;code&gt;10.0.0.0/16&lt;/code&gt;) is just the shorthand for "reserve this block".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cidrsubnet&lt;/code&gt;&lt;/strong&gt; is a built-in Terraform function that carves a smaller CIDR block out of a bigger one.&lt;/li&gt;
&lt;li&gt;We have a loop in there too so 2 AZs in the variable become 2 actual private subnets and 2 public subnets (offset by &lt;code&gt;+ 100&lt;/code&gt; so public and private CIDRs never collide).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;*_subnet_tags&lt;/code&gt; blocks&lt;/strong&gt; is the part that AWS integration of Kubernetes uses to discover &lt;em&gt;which subnets to use&lt;/em&gt; by looking for these exact tag keys:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubernetes.io/role/elb&lt;/code&gt; on public subnets tells the cluster "put internet-facing Load Balancers here".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes.io/role/internal-elb&lt;/code&gt; does the same for internal ones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes.io/cluster/&amp;lt;name&amp;gt; = shared&lt;/code&gt; marks a subnet as belonging to this cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip these tags and &lt;code&gt;type: LoadBalancer&lt;/code&gt; Kubernetes services simply won't provision correctly. Documented here: &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html" rel="noopener noreferrer"&gt;EKS VPC and subnet requirements&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/eks.tf" rel="noopener noreferrer"&gt;&lt;code&gt;eks.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vpc_id = module.vpc.vpc_id&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;subnet_ids = module.vpc.private_subnets&lt;/code&gt;&lt;/strong&gt; is how Terraform wires two modules together, and it's &lt;em&gt;also&lt;/em&gt; how Terraform knows the VPC must be created before the cluster. &lt;code&gt;module.vpc.vpc_id&lt;/code&gt; is an &lt;strong&gt;output&lt;/strong&gt; of the &lt;code&gt;vpc&lt;/code&gt; module (every module can expose outputs the same way the root configuration does, from &lt;code&gt;outputs.tf&lt;/code&gt;). You never had to write "create the VPC first"; Terraform derived that from the fact that &lt;code&gt;eks.tf&lt;/code&gt; reads a value that only exists once &lt;code&gt;vpc.tf&lt;/code&gt; has run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker nodes go in &lt;code&gt;module.vpc.private_subnets&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;enable_cluster_creator_admin_permissions = true&lt;/code&gt;&lt;/strong&gt; is what enables the IAM user who ran &lt;code&gt;terraform apply&lt;/code&gt; to issue &lt;code&gt;kubectl&lt;/code&gt; commands against. This flag registers your IAM identity as a cluster admin automatically via an &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html" rel="noopener noreferrer"&gt;EKS access entry&lt;/a&gt;, which is the modern replacement for the old, fiddlier &lt;code&gt;aws-auth&lt;/code&gt; ConfigMap approach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;access_entries.console_viewer&lt;/code&gt;&lt;/strong&gt; is the entry for IAM user I intend it to be allowed to look at the cluster. &lt;code&gt;enable_cluster_creator_admin_permissions&lt;/code&gt; only ever creates one access entry, for the apply-time identity; a second human who wants to browse pods/logs in the AWS Console needs their own. This block grants exactly that: it maps &lt;code&gt;aws_iam_user.console_viewer&lt;/code&gt;'s ARN (&lt;code&gt;iam.tf&lt;/code&gt;) to AWS's managed &lt;code&gt;AmazonEKSViewPolicy&lt;/code&gt; (read-only, cluster-scoped) via a Kubernetes-RBAC access entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;addons&lt;/code&gt;&lt;/strong&gt; are AWS-managed installs of standard Kubernetes cluster components, so you don't &lt;code&gt;kubectl apply&lt;/code&gt; them yourself: &lt;code&gt;coredns&lt;/code&gt; (in-cluster DNS is how &lt;code&gt;redis&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; hostnames in the app's connection strings actually resolve), &lt;code&gt;kube-proxy&lt;/code&gt; (routes service traffic to the right pod), &lt;code&gt;vpc-cni&lt;/code&gt; (gives every pod a real VPC IP address, the AWS-specific way pod networking works on EKS), and &lt;code&gt;eks-pod-identity-agent&lt;/code&gt; (lets pods assume IAM roles, a near-mandatory addon for anything that'll later talk to S3/DynamoDB/etc. from inside a pod). &lt;code&gt;before_compute = true&lt;/code&gt; on two of them means "install this before the worker nodes even join" since nodes need working pod networking to function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eks_managed_node_groups&lt;/code&gt;&lt;/strong&gt; is the actual EC2 Auto Scaling group your pods run on. &lt;code&gt;AL2023_x86_64_STANDARD&lt;/code&gt; is Amazon Linux 2023, the current default EKS-optimized AMI type. &lt;code&gt;min_size&lt;/code&gt;/&lt;code&gt;max_size&lt;/code&gt;/&lt;code&gt;desired_size&lt;/code&gt; are Auto Scaling group settings, Kubernetes' own autoscaler (if you added one) would adjust &lt;code&gt;desired_size&lt;/code&gt; between the min/max bounds; here they're fixed at whatever &lt;code&gt;terraform.tfvars&lt;/code&gt; says.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;

&lt;p&gt;The Amazon Linux 2023, x86_64 architecture, "standard" (no GPU, no ARM), this is the image we used. Other options include things like &lt;code&gt;AL2023_ARM_64_STANDARD&lt;/code&gt; (Graviton/ARM), or &lt;code&gt;_NVIDIA&lt;/code&gt;/&lt;code&gt;_GPU&lt;/code&gt; variants for GPU workloads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full module docs, including every other option available: &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest" rel="noopener noreferrer"&gt;terraform-aws-modules/eks/aws on the Registry&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/iam.tf" rel="noopener noreferrer"&gt;&lt;code&gt;iam.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;enable_cluster_creator_admin_permissions&lt;/code&gt; covers the automation identity that runs &lt;code&gt;terraform apply&lt;/code&gt;. It deliberately has no AWS Console access. So if you personally want to open the EKS Console and click through pods, clusters, etc, that's a &lt;strong&gt;second, separate&lt;/strong&gt; IAM identity, created in &lt;code&gt;iam.tf&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_iam_user.console_viewer&lt;/code&gt;&lt;/strong&gt;: the IAM user itself, named by default &lt;code&gt;voting-app-console-viewer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_iam_user_policy&lt;/code&gt;&lt;/strong&gt;: grants only enough to &lt;em&gt;reach&lt;/em&gt; the EKS API: the account-level &lt;code&gt;ListClusters&lt;/code&gt;/&lt;code&gt;DescribeClusterVersions&lt;/code&gt;, and &lt;code&gt;DescribeCluster&lt;/code&gt;/&lt;code&gt;AccessKubernetesApi&lt;/code&gt; scoped to this cluster only (&lt;code&gt;module.eks.cluster_arn&lt;/code&gt;). This policy grants permission to AWS Console access; the RBAC half is &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/eks.tf#L19-L30" rel="noopener noreferrer"&gt;the &lt;code&gt;access_entries&lt;/code&gt; block in &lt;code&gt;eks.tf&lt;/code&gt;&lt;/a&gt;. We need both, and they answer different questions ("can this principal call the EKS API at all" vs. "what can it do once inside the cluster").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_iam_user_login_profile&lt;/code&gt;&lt;/strong&gt;, with &lt;code&gt;password_reset_required = true&lt;/code&gt; generates a one-time password and forces a change on first login. In my experience they usually do not do this, instead DevOps team grant permission to each developer appropriate access rights and it is tied to their role in the organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_iam_user_policy_attachment&lt;/code&gt;&lt;/strong&gt; adds AWS's managed &lt;code&gt;IAMUserChangePassword&lt;/code&gt; policy to enable users get self-service &lt;code&gt;iam:ChangePassword&lt;/code&gt; permission to change their own password.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/data.tf" rel="noopener noreferrer"&gt;&lt;code&gt;data.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;data&lt;/code&gt; block, unlike &lt;code&gt;resource&lt;/code&gt;, doesn't create anything.&lt;/li&gt;
&lt;li&gt;It just reads information that already exists, here the AWS account ID of whoever's credentials are active.&lt;/li&gt;
&lt;li&gt;We need it when building the AWS Console sign-in URL in &lt;code&gt;outputs.tf&lt;/code&gt; (&lt;code&gt;https://&amp;lt;account_id&amp;gt;.signin.aws.amazon.com/console&lt;/code&gt;), which requires knowing the account ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/cleanup.tf" rel="noopener noreferrer"&gt;&lt;code&gt;cleanup.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Before we can torn down this cluster's VPC/subnets, we must first use &lt;code&gt;kubectl&lt;/code&gt; and delete &lt;code&gt;voting-service.yaml&lt;/code&gt;/&lt;code&gt;result-service.yaml&lt;/code&gt;. This frees the AWS Load Balancers Kubernetes created for them. Those Load Balancers were never created by a &lt;code&gt;resource&lt;/code&gt; block, so Terraform's state doesn't know they exist and &lt;code&gt;terraform destroy&lt;/code&gt; alone can't remove them. Or at least this is what my lizard brain thinks considering &lt;a href="https://serverfault.com/questions/1103575/terraform-destroy-failing-for-kubernetes-provider-with-pvc-in-aws-eks-how-to-fi" rel="noopener noreferrer"&gt;all the hints I saw here&lt;/a&gt; and &lt;a href="https://stackoverflow.com/questions/76417271/terraform-destroy-on-eks-fails-within-github-actions-workflow" rel="noopener noreferrer"&gt;there&lt;/a&gt;. But still since &lt;a href="https://repost.aws/questions/QU-1eaoDeuThmDyQnj8qCGPQ/do-kubernetes-loadbalancer-services-need-to-be-deleted-before-terraform-destroy-when-eks-is-provisioned-by-terraform-but-nlbs-are-not" rel="noopener noreferrer"&gt;I was not so sure I asked it here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/kasir-barati/docker/blob/948d84f667d60107817b74f00bf50b16505af9fb/k8s/voting-microservice-architecture/deployment/terraform/outputs.tf" rel="noopener noreferrer"&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Outputs are Terraform's way of handing you (or another Terraform config, or a CI pipeline) values it only knows after resources actually exist. Here we print stuff like AWS Console login URL, username, cluster endpoint, etc.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;

&lt;p&gt;When we dump something in the output you can mark it as sensitive so it won't show in the CI/CD pipeline logs in plain text. That is what I did for the temporary password. So if you need to see the value in plain text you must run &lt;code&gt;terraform output -raw console_viewer_initial_password&lt;/code&gt; to actually read it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How the pieces fit together
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TB
    Root["Root module\n(this directory's .tf files)"]
    Root --&amp;gt;|passes vpc_cidr, azs| VPC["module: terraform-aws-modules/vpc/aws"]
    Root --&amp;gt;|passes cluster_name, node sizing| EKS["module: terraform-aws-modules/eks/aws"]
    VPC --&amp;gt;|"outputs: vpc_id, private_subnets"| EKS
    EKS --&amp;gt;|creates| CP[EKS control plane]
    EKS --&amp;gt;|creates| NG[Managed node group]
    EKS --&amp;gt;|creates| IAM[IAM roles for cluster + nodes]
    VPC --&amp;gt;|creates| Net[VPC, subnets, NAT gateway]
    Root --&amp;gt;|"aws_iam_user, policy, login profile"| ConsoleIAM["iam.tf: console_viewer"]
    ConsoleIAM -.ARN referenced by.-&amp;gt; EKS
    Root --&amp;gt;|"null_resource, depends_on module.eks"| Cleanup["cleanup.tf: cleans up LBs at before destroy starts"]&lt;/code&gt;&lt;/pre&gt;



</description>
      <category>terraform</category>
      <category>aws</category>
      <category>eks</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Looking at what we are Building</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:25:59 +0000</pubDate>
      <link>https://dev.to/kasir-barati/looking-at-what-we-are-building-1flf</link>
      <guid>https://dev.to/kasir-barati/looking-at-what-we-are-building-1flf</guid>
      <description>&lt;p&gt;So now that you have a &lt;a href="https://dev.to/kasir-barati/terraform-basics-32hf"&gt;basic understanding of how Terraform works&lt;/a&gt;, before you start running any &lt;code&gt;terraform&lt;/code&gt; command against a real AWS account, two things need to happen: you need an identity Terraform can authenticate as, and you need a mental picture of what you're about to create, so the plan output in Part 4 isn't just a list of unfamiliar resource names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never Use Your AWS Root User
&lt;/h2&gt;

&lt;p&gt;The root user (the email/password you signed up to AWS with) can do &lt;em&gt;anything&lt;/em&gt;, including closing the account. It should basically never be used day-to-day. Instead, create a dedicated &lt;strong&gt;IAM user&lt;/strong&gt; just for this project. In real life you would create a dedicated IAM user for your CI/CD pipeline to automate deployments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Console → &lt;strong&gt;IAM&lt;/strong&gt; → &lt;strong&gt;Users&lt;/strong&gt; → &lt;strong&gt;Create user&lt;/strong&gt; (e.g. &lt;code&gt;terraform-voting-app&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;not&lt;/strong&gt; enable AWS Console access, this user only needs &lt;em&gt;programmatic&lt;/em&gt; access, i.e. an API key pair.&lt;/li&gt;
&lt;li&gt;The AWS managed policy &lt;strong&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt;&lt;/strong&gt; is the path of least friction, and is what you should use for the IAM user to test things out. but in real life you would go with least privilege approach, learn more about it in &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/security_iam_id-based-policy-examples.html" rel="noopener noreferrer"&gt;AWS EKS IAM policy examples&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;On the user's &lt;strong&gt;Security credentials&lt;/strong&gt; tab → &lt;strong&gt;Create access key&lt;/strong&gt; → choose "Command Line Interface (CLI)". You'll get an &lt;strong&gt;Access Key ID&lt;/strong&gt; and a &lt;strong&gt;Secret Access Key&lt;/strong&gt;, store them somewhere safe, we will be needing them later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Give Terraform those Credentials
&lt;/h2&gt;

&lt;p&gt;The rule: &lt;strong&gt;credentials never go inside a &lt;code&gt;.tf&lt;/code&gt; file, and never inside &lt;code&gt;terraform.tfvars&lt;/code&gt;.&lt;/strong&gt; So in your local machine or CI/CD pipeline you need to export &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, and &lt;code&gt;AWS_REGION&lt;/code&gt; as environment variables in the shell. Then you won't be needing &lt;code&gt;aws configure&lt;/code&gt; or &lt;code&gt;aws login&lt;/code&gt; step anywhere, &lt;a href="https://github.com/kasir-barati/docker/blob/f95d3232e8233de98ba11bf8788537be5904a581/k8s/voting-microservice-architecture/deployment/terraform/providers.tf" rel="noopener noreferrer"&gt;the &lt;code&gt;aws&lt;/code&gt; provider&lt;/a&gt; has no &lt;code&gt;access_key&lt;/code&gt;/&lt;code&gt;secret_key&lt;/code&gt; arguments of its own, so it falls back to the AWS SDK's standard credential chain, which checks these exact environment variables first.&lt;/p&gt;

&lt;p&gt;The AWS CLI and, later &lt;code&gt;kubectl&lt;/code&gt; read the same variables. In my local machine I do &lt;a href="https://github.com/kasir-barati/docker/tree/f95d3232e8233de98ba11bf8788537be5904a581/k8s/voting-microservice-architecture/deployment/terraform#12-give-terraform-those-credentials" rel="noopener noreferrer"&gt;&lt;code&gt;export&lt;/code&gt; an env variable files using a shell script&lt;/a&gt;. The export only lasts for the current shell session, re-run it in any new terminal before running &lt;code&gt;terraform&lt;/code&gt;/&lt;code&gt;aws&lt;/code&gt;/&lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You are About to Build
&lt;/h2&gt;

&lt;p&gt;This is the part most beginner Terraform/EKS tutorials skip, and it's the part that makes the actual &lt;code&gt;.tf&lt;/code&gt; files make sense at a glance instead of feeling like a wall of unfamiliar arguments.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph AWS["AWS Account / Region"]
        subgraph VPC["VPC — 10.0.0.0/16"]
            IGW["Internet Gateway"]

            subgraph AZ1["Availability Zone A"]
                PubA["Public subnet\n10.0.100.0/24"]
                PrivA["Private subnet\n10.0.0.0/24"]
            end

            subgraph AZ2["Availability Zone B"]
                PubB["Public subnet\n10.0.101.0/24"]
                PrivB["Private subnet\n10.0.1.0/24"]
            end

            NAT["NAT Gateway\n(in a public subnet)"]

            CP["EKS Control Plane\n(managed by AWS,\nnot inside your subnets)"]

            PrivA --- Node1["EC2 worker node"]
            PrivB --- Node2["EC2 worker node"]
        end

        NLB["Network Load Balancer\n(public, one per exposed Service)"]
    end

    Internet(("Internet")) --&amp;gt; IGW
    IGW --&amp;gt; PubA
    IGW --&amp;gt; PubB
    PubA --&amp;gt; NAT
    NAT -.outbound only.-&amp;gt; Node1
    NAT -.outbound only.-&amp;gt; Node2
    CP &amp;lt;-. manages .-&amp;gt; Node1
    CP &amp;lt;-. manages .-&amp;gt; Node2
    Internet --&amp;gt; NLB
    NLB --&amp;gt; Node1
    NLB --&amp;gt; Node2

    Node1 -- runs --&amp;gt; Pods1["voting / result / worker\nredis / postgres pods"]
    Node2 -- runs --&amp;gt; Pods2["voting / result / worker\nredis / postgres pods"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Reading it top to bottom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC&lt;/strong&gt;: a private network inside AWS, &lt;code&gt;10.0.0.0/16&lt;/code&gt; here (65k addresses is just the conventional default and more than what we need).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two Availability Zones&lt;/strong&gt;: EKS requires subnets in at least 2 AZs, so a single AZ failure can't take the whole cluster down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public subnets&lt;/strong&gt; have a route to the Internet Gateway. Their only job is hosting the NAT Gateway and, later, the public-facing Load Balancers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private subnets&lt;/strong&gt; is where the actual EC2 worker nodes live. They have &lt;strong&gt;no direct route in from the internet&lt;/strong&gt;, and no public IP at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT Gateway&lt;/strong&gt; lets the private-subnet nodes reach &lt;em&gt;out&lt;/em&gt; to the internet (to pull container images, talk to the EKS API, etc.) without allowing anything in from the internet. One-way door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EKS Control Plane&lt;/strong&gt; is a managed AWS service which is the Kubernetes API server, scheduler, etc. It doesn't live "in" your subnets the way an EC2 instance does, though it does attach elastic network interfaces into them to talk to your nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker nodes&lt;/strong&gt; are plain EC2 instances, sitting in the private subnets, that the EKS control plane schedules your pods onto. This is the "data plane", as opposed to the control plane above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Load Balancer&lt;/strong&gt; is created later, &lt;em&gt;by Kubernetes&lt;/em&gt; (not Terraform) when you expose a Service as &lt;code&gt;type: LoadBalancer&lt;/code&gt;. This is how traffic actually reaches your app from a browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  EKS Costs
&lt;/h3&gt;

&lt;p&gt;We have two separate charges, both starting the moment &lt;code&gt;terraform apply&lt;/code&gt; finishes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;EKS control plane&lt;/strong&gt; itself has a flat hourly rate. Check &lt;a href="https://aws.amazon.com/eks/pricing/" rel="noopener noreferrer"&gt;current EKS pricing&lt;/a&gt; and this is regardless of whether any pods are running.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;EC2 worker nodes&lt;/strong&gt;: ordinary EC2 billing, because they're ordinary EC2 instances. Two &lt;code&gt;t3.medium&lt;/code&gt; nodes, in this project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus a NAT Gateway (hourly + per-GB processed) and small EBS volumes for each node's disk. None of it is free-tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html" rel="noopener noreferrer"&gt;IAM: creating an IAM user&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/eks/pricing/" rel="noopener noreferrer"&gt;AWS EKS pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html" rel="noopener noreferrer"&gt;Amazon VPC concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html" rel="noopener noreferrer"&gt;How Amazon EKS works&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>eks</category>
      <category>iac</category>
    </item>
    <item>
      <title>Terraform Basics</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sat, 05 Sep 2026 17:22:43 +0000</pubDate>
      <link>https://dev.to/kasir-barati/terraform-basics-32hf</link>
      <guid>https://dev.to/kasir-barati/terraform-basics-32hf</guid>
      <description>&lt;p&gt;This is the first part of a real Terraform setup that provisions a brand new Amazon EKS cluster (VPC, control plane, worker nodes — everything, no pre-existing cluster assumed) and deploys a small multi-service voting app onto it. It assumes &lt;strong&gt;no prior Terraform knowledge&lt;/strong&gt;, and I use Kubernetes manifests to deploy the app.&lt;/p&gt;

&lt;p&gt;The actual Terraform code, and the plain Kubernetes manifests, live at &lt;a href="https://github.com/kasir-barati/docker/tree/main/k8s/voting-microservice-architecture/deployment" rel="noopener noreferrer"&gt;&lt;code&gt;k8s/voting-microservice-architecture/deployment&lt;/code&gt;&lt;/a&gt; in my &lt;code&gt;docker&lt;/code&gt; repo. Feel free to give it a star 😉.&lt;/p&gt;

&lt;p&gt;If you've ever created an AWS resource by clicking through the console, or run a pile of &lt;code&gt;aws ec2 ...&lt;/code&gt;/&lt;code&gt;eksctl create cluster&lt;/code&gt; commands from a half-remembered README you've felt the problem Terraform solves: &lt;strong&gt;there is no reliable record of what you built, how to recreate it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Terraform's job is to fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Infrastructure as Code" Actually Means
&lt;/h2&gt;

&lt;p&gt;You write down, in plain text files, &lt;strong&gt;what should exist&lt;/strong&gt;: "a VPC with this CIDR," "an EKS cluster with 2 worker nodes", ...&lt;/p&gt;

&lt;p&gt;You do not write the steps to create it. That distinction matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Imperative&lt;/strong&gt; (a shell script, &lt;code&gt;eksctl&lt;/code&gt;, clicking in the console): you describe the &lt;em&gt;actions&lt;/em&gt;, create this, then that, then attach this to that. If you run it twice, you either get an error ("already exists") or a duplicate 🥲.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative&lt;/strong&gt; (Terraform, and Kubernetes too): you describe the &lt;em&gt;end state&lt;/em&gt;. You run the same command as many times as you like; Terraform figures out the difference between "what exists" and "what you asked for", and only changes what's necessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever written a Kubernetes YAML manifest and run &lt;code&gt;kubectl apply -f .&lt;/code&gt; more than once, you already understand declarative infrastructure. Terraform applies the identical idea one layer down, to the cloud resources underneath the cluster (or, as in this project, &lt;em&gt;including&lt;/em&gt; the cluster itself).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Building Blocks (HCL)
&lt;/h2&gt;

&lt;p&gt;Terraform's config language is called HCL (HashiCorp Configuration Language). You'll see four kinds of blocks constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. A provider - which cloud/API Terraform should talk to&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 2. A resource - one real thing to create&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-unique-bucket-name"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 3. A variable - an input you can change without editing the file&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 4. An output - a value Terraform prints after it's done&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely most of the language. Everything else, loops (&lt;code&gt;for_each&lt;/code&gt;/&lt;code&gt;count&lt;/code&gt;), conditionals, functions like &lt;code&gt;cidrsubnet()&lt;/code&gt; are sugar on top of "declare resources, wire them together with references".&lt;/p&gt;

&lt;p&gt;One more block type you'll see a lot in this project is &lt;strong&gt;&lt;code&gt;module&lt;/code&gt;&lt;/strong&gt;. A module is just a folder of &lt;code&gt;.tf&lt;/code&gt; files someone else already wrote and published, that you can reuse instead of writing hundreds of lines of &lt;code&gt;resource&lt;/code&gt; blocks yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/vpc/aws"&lt;/span&gt;   &lt;span class="c1"&gt;# published on the Terraform Registry&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt;
  &lt;span class="nx"&gt;cidr&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood a module is exactly the same &lt;code&gt;resource&lt;/code&gt;/&lt;code&gt;variable&lt;/code&gt;/&lt;code&gt;output&lt;/code&gt; blocks you just saw, someone packaged them up so you don't have to setup "a correct VPC" from scratch each time. More on &lt;a href="https://registry.terraform.io/" rel="noopener noreferrer"&gt;the Terraform Registry&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: 4 Commands
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Write Terraform Config files\n(desired state)"] --&amp;gt; B["terraform init\ndownload providers/modules"]
    B --&amp;gt; C["terraform plan\npreview: create / change / destroy?"]
    C --&amp;gt; D["terraform apply\nactually call the AWS API"]
    D --&amp;gt; E[("terraform.tfstate\nrecord of what exists")]
    E -.compared against.-&amp;gt; C
    D --&amp;gt; F["terraform destroy\ntear it all back down"]
    F -.reads.-&amp;gt; E&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/strong&gt; downloads the providers and modules your config references. We run it once per directory, and again whenever we add/change a provider/module version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/strong&gt; asks the AWS API "what exists right now that you're tracking?", diffs that against your &lt;code&gt;.tf&lt;/code&gt; files, and prints what would change. &lt;strong&gt;Nothing is created or changed by &lt;code&gt;plan&lt;/code&gt;.&lt;/strong&gt; Always read the plan before applying, it's the single best safety net Terraform gives you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/strong&gt; shows what it will change, asks you to type &lt;code&gt;yes&lt;/code&gt;, then actually calls the API of AWS or any other provider. Terraform works out the correct order on its own (e.g. it won't try to create a subnet before the VPC that subnet belongs to) by following the references between your resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/strong&gt; deletes everything Terraform currently has in its state, in dependency order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  State: The One File You Need to Ensure is Never Lost
&lt;/h2&gt;

&lt;p&gt;Every time you &lt;code&gt;apply&lt;/code&gt;, Terraform writes a file which by default is called &lt;code&gt;terraform.tfstate&lt;/code&gt;. It records exactly what it created. E.g. when provisioning AWS services it keeps a track of the real AWS IDs of each resource (this VPC's actual &lt;code&gt;vpc-0abc123...&lt;/code&gt;, that cluster's actual ARN, ...).&lt;/p&gt;

&lt;p&gt;This file is &lt;em&gt;the&lt;/em&gt; thing that makes Terraform work. Without it, Terraform has no way to know "I already made this VPC, don't make another one" or "if you ask me to destroy, here's exactly what to delete". A few rules that follow directly from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never hand-edit it.&lt;/strong&gt; Use &lt;code&gt;terraform state&lt;/code&gt; subcommands if you truly need to intervene.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never commit it to a public repo.&lt;/strong&gt; It can contain sensitive values (e.g. database passwords set via a resource argument) in plain text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't run &lt;code&gt;apply&lt;/code&gt; from two places at once against the same state file.&lt;/strong&gt; Two concurrent applies can corrupt it. (Real teams solve this with a &lt;em&gt;remote backend&lt;/em&gt;, e.g. state is stored in a S3 bucket, or in Terraform Cloud. &lt;a href="https://developer.hashicorp.com/terraform/language/backend" rel="noopener noreferrer"&gt;Learn more about Terraform backends&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you delete it, Terraform "forgets" everything it made!&lt;/strong&gt; It won't know how to clean those resources when you run &lt;code&gt;terraform destroy&lt;/code&gt; anymore, and it'll try to create duplicates on the next &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go Deeper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/intro" rel="noopener noreferrer"&gt;Terraform's own "What is Terraform?" docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/syntax/configuration" rel="noopener noreferrer"&gt;HCL syntax reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://registry.terraform.io/" rel="noopener noreferrer"&gt;Terraform Registry&lt;/a&gt; — where providers and modules are published&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state" rel="noopener noreferrer"&gt;Terraform state docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>iac</category>
      <category>aws</category>
      <category>terraform</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>The Pipeline Pattern</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sun, 30 Aug 2026 13:55:51 +0000</pubDate>
      <link>https://dev.to/kasir-barati/the-pipeline-pattern-15j5</link>
      <guid>https://dev.to/kasir-barati/the-pipeline-pattern-15j5</guid>
      <description>&lt;p&gt;I watched a nice video about a small multi-stage scraper: it was basically scraping &lt;a href="https://en.wikipedia.org/wiki/List_of_S%26P_500_companies" rel="noopener noreferrer"&gt;S&amp;amp;P 500 tickers from Wikipedia&lt;/a&gt;, fetch a live price for each from &lt;a href="https://finance.yahoo.com/quote/AAPL/" rel="noopener noreferrer"&gt;Yahoo Finance&lt;/a&gt;, and write the results to Postgres. Each stage runs its own pool of worker threads, and stages talk to each other through queues. So I just decided to write about it since it is a nice distributed-systems design pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters" rel="noopener noreferrer"&gt;Pipes &amp;amp; Filters (AKA The Pipeline Pattern)&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is a classic &lt;strong&gt;architectural pattern&lt;/strong&gt;: a series of independent processing stages ("filters"), each connected to the next by a channel ("pipe"). Each stage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads from an inbound queue.&lt;/li&gt;
&lt;li&gt;Does one unit of work.&lt;/li&gt;
&lt;li&gt;Writes to an outbound queue.&lt;/li&gt;
&lt;li&gt;And knows nothing about the stages before or after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first stage (no inbound queue) is the &lt;strong&gt;source&lt;/strong&gt; or &lt;strong&gt;producer&lt;/strong&gt;. The last stage (no outbound queue) is the &lt;strong&gt;sink&lt;/strong&gt; or &lt;strong&gt;consumer&lt;/strong&gt;. Everything in between is just a filter.&lt;/p&gt;

&lt;p&gt;In the following example we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WikiWorker (source)
    -&amp;gt; symbol_queue
        -&amp;gt; YahooFinancePriceScheduler x4 (filter, fanned out)
            -&amp;gt; postgres_queue
                -&amp;gt; PostgresMasterScheduler x4 (sink, fanned out)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example which I believe is a strong candidate for this design pattern is when you have a pipeline for raw data, sending them to different LLM-powered apps to extract and generate useful insights while maintaining a clean architecture which is scalable and maintainable. LLMs are slow, I/O-bound, rate-limited, and failure-prone (timeouts, malformed JSON, rate limit errors), and you often want to run several different extraction tasks over the same raw data.&lt;/p&gt;

&lt;p&gt;A natural shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source: raw data ingestion (files, API, scrape, DB change stream)
    -&amp;gt; raw_queue
        -&amp;gt; Preprocessing stage (clean/chunk/normalize) x N workers
            -&amp;gt; llm_queue
                -&amp;gt; LLM extraction workers x N (fan-out: one pool per "app" -- summarizer, entity extractor, sentiment, classifier, etc.)
                    -&amp;gt; results_queue(s)
                        -&amp;gt; Sink: persistence / downstream index / notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fan-out step is the interesting part: a single normalized item can be pushed to multiple output queues, one per LLM-powered consumer, the same broadcast-to-all-output-queues trick our scraper already does. Each "app" (summarizer, extractor, classifier) becomes an independent filter stage with its own worker pool and its own scaling knob.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits of this Design Pattern
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent scaling per stage&lt;/strong&gt;: LLM calls are the bottleneck, not ingestion. You can run 2 preprocessing workers and 20 LLM-call workers without touching the rest of the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation of failure and backpressure&lt;/strong&gt;: A stalled or rate-limited LLM provider backs up its own queue instead of crashing ingestion or the other extraction apps running in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy horizontal fan-out to multiple "apps"&lt;/strong&gt;: Adding a new insight-extraction app is just: add a new output queue + a new worker pool. No changes to upstream stages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural retry/DLQ boundaries&lt;/strong&gt;: Since each stage only knows its own queue, you can wrap an LLM stage with its own retry logic or dead-letter queue without leaking that complexity into ingestion or persistence code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable transport&lt;/strong&gt;: In-memory Queue today, Kafka/RabbitMQ/SQS tomorrow, the stage logic doesn't change, only what's plugged into the pipe. That's what "clean architecture" buys you here: stages depend on queue interfaces, not on each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability/testability&lt;/strong&gt;. Each worker class is testable in isolation, feed it a fake input queue, assert what lands on the output queue. No need to spin up the whole pipeline to unit test one extraction stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost/throughput control point&lt;/strong&gt;: Because concurrency is explicit per stage (pool size), it's a direct lever for controlling how hard you hit an LLM API's rate limits, independent of ingestion speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This design pattern itself is architecture-agnostic. It's just stages + queues; whether those stages are threads in one process, services in a monorepo, or separate microservices is an implementation detail, not a change to the pattern. You do always need some orchestrator, though its job shrinks as you move outward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monolith/threads&lt;/strong&gt;: you have a orchestrator (in our scraper example we have &lt;code&gt;main.py&lt;/code&gt;), which owns queue creation, worker counts, and shutdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo/multi-process&lt;/strong&gt;: orchestrator becomes a supervisor process or a broker's own routing (Kafka/RabbitMQ take over "the pipe").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservices + managed inference (&lt;a href="https://aws.amazon.com/sagemaker/" rel="noopener noreferrer"&gt;SageMaker&lt;/a&gt; etc.)&lt;/strong&gt;: orchestrator is very thin, it just decides how many in-flight requests it allows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dual autoscaling concern&lt;/strong&gt;: you now have concurrency control in two places, your orchestrator's queue-consumption concurrency, and SageMaker's endpoint autoscaling. And they aren't aware of each other. Imagine your orchestrator can burst requests faster than SageMaker's endpoint scales up (429s/throttling), or SageMaker scales up while your orchestrator is deliberately throttled, wasting provisioned capacity.&lt;/p&gt;

&lt;p&gt;The fix is to make one the source of truth (SageMaker), and the orchestrator reacts to them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have your orchestrator's concurrency be a ceiling tuned to your endpoint's steady-state capacity, not an independent guess.&lt;/li&gt;
&lt;li&gt;Let SageMaker's autoscaling handle burst absorption, and have your orchestrator back off on throttling responses (retry-with-backoff) rather than pre-guessing SageMaker's scale.

&lt;ul&gt;
&lt;li&gt;"Pre-guessing" = hardcoding a concurrency number based on an assumption like "SageMaker probably has 10 instances up, so I'll send 10x work."&lt;/li&gt;
&lt;li&gt;Instead of polling "how many instances exist", react to the endpoint's actual signal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Optionally drive orchestrator concurrency from an external signal (endpoint's current instance count / concurrency metric) instead of a static number.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Pieces this Pattern is Made of
&lt;/h2&gt;

&lt;p&gt;A few sub-patterns show up together whenever you build a pipeline like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Producer-consumer&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;A queue serves as a buffer between two parts of our app. One puts data in the queue, and the other takes them out.&lt;/li&gt;
&lt;li&gt;E.g. &lt;code&gt;WikiWorker&lt;/code&gt; puts ticker symbols into &lt;code&gt;symbol_queue&lt;/code&gt;. The Yahoo Finance workers take symbols out of it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WikiWorker&lt;/code&gt; doesn't need Yahoo to be ready, and Yahoo doesn't need Wiki to still be running&lt;/li&gt;
&lt;li&gt;The queue absorbs the timing mismatch.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker pool&lt;/strong&gt;: Instead of having just one thread handle a stage, you run several identical threads that all pull from the same queue, so the work gets split among them automatically..&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out / fan-in&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out&lt;/strong&gt; spreads work across workers:&lt;/li&gt;
&lt;li&gt;One queue.&lt;/li&gt;
&lt;li&gt;Many workers reading from it (this is the worker pool from above, "fanning out" work across threads).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-in&lt;/strong&gt; collects workers' output back into one place:&lt;/li&gt;
&lt;li&gt;Many workers.&lt;/li&gt;
&lt;li&gt;All workers write into the same single downstream queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Sentinel_value" rel="noopener noreferrer"&gt;&lt;strong&gt;Poison pill (sentinel value)&lt;/strong&gt;&lt;/a&gt;: a special marker value, here it is &lt;code&gt;'DONE'&lt;/code&gt; string. Pushed onto a queue to tell every consumer "there's nothing more coming, exit your loop". It's the standard way to shut down a pipeline gracefully instead of killing threads abruptly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Same Design Patterns in Other Programming Languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;, using goroutines and channels, &lt;a href="https://go.dev/blog/pipelines" rel="noopener noreferrer"&gt;their article&lt;/a&gt; on this is basically the canonical writeup: it describes a pipeline as a series of stages connected by channels, each stage a group of goroutines running the same function, with a &lt;code&gt;done&lt;/code&gt; channel used for the poison-pill/cancellation (also read &lt;a href="https://ketansingh.me/posts/pipeline-pattern-in-go-part-1/" rel="noopener noreferrer"&gt;this&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt;, using &lt;a href="https://www.geeksforgeeks.org/java/blockingqueue-interface-in-java/" rel="noopener noreferrer"&gt;&lt;code&gt;BlockingQueue&lt;/code&gt;&lt;/a&gt; and thread pools (&lt;a href="https://www.geeksforgeeks.org/java/java-util-concurrent-executorservice-interface-with-examples/" rel="noopener noreferrer"&gt;&lt;code&gt;ExecutorService&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unix shell pipelines&lt;/strong&gt; (&lt;code&gt;cmd1 | cmd2 | cmd3&lt;/code&gt;), the original inspiration for the name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message brokers&lt;/strong&gt; like Kafka or RabbitMQ, where "queue" becomes a literal broker topic/queue instead of an in-memory object, and stages become separate services instead of threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying idea; decouple stages, connect them with buffered channels, parallelize each stage independently is a general concurrency and distributed-systems pattern. Python's &lt;code&gt;queue.Queue&lt;/code&gt;/&lt;code&gt;multiprocessing.Queue&lt;/code&gt; is just one convenient implementation of "the pipe" part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scraper Example
&lt;/h2&gt;

&lt;p&gt;I believe the database schema is more than obvious but just to leave no room to the imagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;        &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;insert_time&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did NOT sanity check if the scaping still is correct since I wanted to talk about the design pattern and not the scraping itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;src/workers/wiki.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Scrapes the current S&amp;amp;P 500 constituent list from Wikipedia.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WikiWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetches and parses the S&amp;amp;P 500 constituents table from Wikipedia.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://en.wikipedia.org/wiki/List_of_S%26P_500_companies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_company_symbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield ticker symbols parsed out of the constituents table HTML.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;constituents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;table_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table_row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table_rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
            &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table_row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;td&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_sp_500_companies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield each S&amp;amp;P 500 ticker symbol, or nothing on request failure.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Couldn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t get entries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_extract_company_symbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;src/workers/yahoo_finance_price.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Worker pool that fetches live prices from Yahoo Finance.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;lxml&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;

&lt;span class="n"&gt;Symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;span class="n"&gt;PriceMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Sentinel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YahooFinancePriceScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Pulls ticker symbols off an input queue and fans price results out
    to one or more output queues.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;input_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queue[Symbol | Sentinel]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;output_queues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queue[PriceMessage | Sentinel]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_queue&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_queues&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Defensive programming: protects this scheduler from hanging
&lt;/span&gt;            &lt;span class="c1"&gt;# indefinitely. Trade-off: if a message arrives right after the
&lt;/span&gt;            &lt;span class="c1"&gt;# timeout fires, it's dropped without ever being processed.
&lt;/span&gt;            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timeout reached in Yahoo Finance scheduler, stopping...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;output_queue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_queues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YahooFinancePriceWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_price&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PriceMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;output_queue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_output_queues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# Cloudflare may block bursty requests
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YahooFinancePriceWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetches the current price for a single ticker symbol.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://finance.yahoo.com/quote/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_symbol&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the current price, or None if the request/parse fails.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="n"&gt;page_contents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_contents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;//*[@id=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote-header-info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]/div[3]/div[1]/div/span[1]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="n"&gt;raw_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;src/workers/postgres.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Worker pool that persists price messages into Postgres.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="n"&gt;Symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;span class="n"&gt;PriceMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Sentinel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresMasterScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Consumes price messages from a queue and writes each one to Postgres.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Queue[PriceMessage | Sentinel]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_queue&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_input_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timeout reached in Postgres scheduler, stopping...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extracted_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;
            &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostgresWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extracted_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_into_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Inserts a single price observation into the `prices` table.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extracted_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_extracted_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extracted_time&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PG_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_pw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PG_PW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PG_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PG_DB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_pw&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pg_db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;insert_into_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;insert_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
            INSERT INTO prices (symbol, price, insert_time)
            VALUES (:symbol, :price, CAST(:extracted_time AS TIMESTAMP))
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;insert_query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_extracted_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;src/main.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wires the wiki -&amp;gt; Yahoo Finance -&amp;gt; Postgres pipeline together and runs it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;workers.postgres&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PostgresMasterScheduler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;workers.wiki&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WikiWorker&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;workers.yahoo_finance_price&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YahooFinancePriceScheduler&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;symbol_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;postgres_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;scraper_start_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;wiki_worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WikiWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;num_yahoo_finance_price_workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="n"&gt;yahoo_finance_price_scheduler_threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;YahooFinancePriceScheduler&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;# Imagine multiple output queues here: one for Redis, one for
&lt;/span&gt;        &lt;span class="c1"&gt;# RabbitMQ, etc. Each Yahoo worker fans its result out to all of them.
&lt;/span&gt;        &lt;span class="nc"&gt;YahooFinancePriceScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;input_queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;symbol_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_queues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;postgres_queue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_yahoo_finance_price_workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;num_postgres_workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="n"&gt;postgres_scheduler_threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PostgresMasterScheduler&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;PostgresMasterScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;postgres_queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_postgres_workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;wiki_worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_sp_500_companies&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;symbol_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yahoo_finance_price_scheduler_threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;symbol_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DONE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;yahoo_finance_price_scheduler_threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;postgres_scheduler_threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;scraper_start_time&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finished in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  More Mature Pipelines
&lt;/h2&gt;

&lt;p&gt;So I guess you have seen it now that if you need to add a new stage/worker to the existing orchestrator you must change the code and write a bunch of code which really feels like boilerplates. So next what you can do is defining your pipeline in a YAML file and then each time you have a new stage/worker you can just need to add it there. So create a &lt;code&gt;src/pipelines&lt;/code&gt; directory and inside it create a yaml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/pipelines/wiki_yahoo_scraper_pipeline.yaml&lt;/span&gt;
&lt;span class="na"&gt;queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SymbolQueue&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Contains symbols/tickers to be scraped from Yahoo&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresUploading&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Contains data that needs to be uploaded to Postgres&lt;/span&gt;

&lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WikiWorker&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This scraps Wikipedia page nad extracts symbols/tickets&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workers.wiki&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WikiWorkerMasterScheduler&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;input_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://en.wikipedia.org/wiki/List_of_S%26P_500_companies&lt;/span&gt;
    &lt;span class="na"&gt;output_queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SymbolQueue&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YahooFinanceWorker&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pulls the stock price of a given ticket from Yahoo Finance&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workers.yahoo_finance_price&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YahooFinancePriceScheduler&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="na"&gt;input_queue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SymbolQueue&lt;/span&gt;
    &lt;span class="na"&gt;output_queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PostgresUploading&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresWorker&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stores stock data in Postgres&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workers.postgres&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresMasterScheduler&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="na"&gt;input_queue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PostgresUploading&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So as you can see with this you are effectively moving the logic of your pipeline into a config file which is written in YAML. And this makes your orchestrator cleaner and you can easily reason about your pipeline without having to read a bunch of python code. Also it makes it easier to differentiate between when you wanna e.g. change the URL you use for scraping data from it, from implementation changes you had to make to the code itself.&lt;/p&gt;

&lt;p&gt;Easier time to review and understand what was changed in a PR/commit. And now your &lt;code&gt;src/main.py&lt;/code&gt; would look a lot nicer with less manual steps involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wires the wiki -&amp;gt; Yahoo Finance -&amp;gt; Postgres pipeline together and runs it.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipelines.reader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YamlPipelineExecutor&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scraper_start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pipeline_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipelines&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wiki_yahoo_scraper_pipeline.yaml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;yaml_pipeline_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YamlPipelineExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pipeline_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;yaml_pipeline_executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;scraper_start_time&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finished in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did NOT add &lt;code&gt;YamlPipelineExecutor&lt;/code&gt; code here. But that was not the only change I had to make to make this pipeline work with the aforementioned yaml file. You can see the &lt;a href="https://github.com/kasir-barati/python/tree/bf47c7b1c9b629e24b6730e01ea0e3ea4e43c074/tips/examples/scrapper-pipeline-design-pattern" rel="noopener noreferrer"&gt;complete example here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you've built something similar, especially across process or machine boundaries with a real broker instead of an in-memory queues I'd love to hear how you handled backpressure and shutdown.&lt;/p&gt;

</description>
      <category>python</category>
      <category>systemdesign</category>
      <category>concurrency</category>
      <category>architecture</category>
    </item>
    <item>
      <title>RabbitMQ Consumer as a Separate Worker Service</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Sun, 30 Aug 2026 09:04:59 +0000</pubDate>
      <link>https://dev.to/kasir-barati/rabbitmq-consumer-as-a-separate-worker-service-adc</link>
      <guid>https://dev.to/kasir-barati/rabbitmq-consumer-as-a-separate-worker-service-adc</guid>
      <description>&lt;p&gt;I come from a NodeJS background and honestly there we usually consume messages in the same NestJS app. But 2 years ago (2024) when I started to develop Backend APIs in Python I realized there is a difference in programming language paradigms. In Python I had to either use &lt;a href="https://docs.python.org/3/library/asyncio.html" rel="noopener noreferrer"&gt;asyncio&lt;/a&gt;/&lt;a href="https://docs.python.org/3/library/threading.html" rel="noopener noreferrer"&gt;threads&lt;/a&gt;/&lt;a href="https://docs.python.org/3/library/multiprocessing.html" rel="noopener noreferrer"&gt;processes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is an "at scale" alternative to &lt;a href="https://github.com/kasir-barati/python/blob/main/tips/multithreading.md" rel="noopener noreferrer"&gt;the multithreading write-up I did&lt;/a&gt; in particular (but I believe you can get some inspiration even if you have a subprocess) instead of a background thread living inside a GraphQL API process, the RabbitMQ consumer has its own container, and its own set of configuration. So the flow would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────┐
│ RabbitMQ                │
└─────────────────────────┘
        |
        ▼
┌─────────────────────────┐
| Worker process          |
└─────────────────────────┘
        |
        ▼
┌─────────────────────────┐
| Fetch/create user       |
└─────────────────────────┘
        |
        | "At least once" delivery guarantee
        ▼
┌─────────────────────────┐
| Redis pub/sub           |
└─────────────────────────┘
        |
        ▼
┌─────────────────────────┐
| Strawberry subscription |
└─────────────────────────┘
        |
        ▼
┌─────────────────────────┐
| GraphQL client          |
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Split It Out at All?
&lt;/h2&gt;

&lt;p&gt;A thread, or subprocess, or asyncio tasks in the GraphQL API process consumer ties three things together that don't actually belong together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scaling dimension.&lt;/strong&gt; HTTP replica count and RabbitMQ consumer count become the same number. If you need 10 API replicas for request load but only 2 consumers' worth of queue throughput, you either over-consume or under-provision HTTP, because one knob controls both. You can only twiddle one knob to adjust two different thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure domain.&lt;/strong&gt; An unhandled exception, a slow leak, or an OOM in message processing takes the HTTP server down with it (or vice versa: redeploying the API for an unrelated change restarts the consumer too, with all the reconnect/redelivery churn that implies).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource shape.&lt;/strong&gt; A CPU-heavy message handler competes with request handling for the same GIL when we create a thread to process RabbitMQ messages. And the same container's CPU/memory limits is shared between the two, instead of getting their own, independently sized deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service#consumer" rel="noopener noreferrer"&gt;A standalone worker process&lt;/a&gt; decouples all 3: scale it on queue depth, deploy/restart it without touching the API, and size its container for the work it actually does.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This more about provisioning, maintaining, horizontal scaling, and monitoring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  "But in NodeJS/&lt;a href="https://www.npmjs.com/package/express" rel="noopener noreferrer"&gt;Express&lt;/a&gt;/&lt;a href="https://www.npmjs.com/package/fastify" rel="noopener noreferrer"&gt;Fastify&lt;/a&gt;/&lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt; we just run it alongside the app"
&lt;/h2&gt;

&lt;p&gt;That's a fair observation, and it works for a while for the same reason the same approach would works for a while in Python: at low volume, none of the three couplings above are painful yet. A few things make it look more tenable in Node than it might seem in Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NodeJS's single-threaded, non-blocking I/O model means an &lt;code&gt;async&lt;/code&gt; RabbitMQ handler naturally interleaves with request handling without needing an extra thread or process at all, so there's no GIL-contention story like there is with a &lt;em&gt;blocking&lt;/em&gt; library such as &lt;code&gt;pika&lt;/code&gt; running in a Python thread.&lt;/li&gt;
&lt;li&gt;Frameworks like NestJS ship first-class support for bolting a message consumer onto the same app (&lt;code&gt;@nestjs/microservices&lt;/code&gt;' hybrid application mode), so it is the path of least resistance, not something bolted on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that removes the scaling/failure-domain coupling, though: a NodeJS/NestJS process consuming RabbitMQ inline still ties consumer count to HTTP replica count, and a handler that blocks the event loop (a CPU-bound computation, a synchronous call, a bad regex) stalls HTTP requests exactly like it would in any single-process design. NodeJS/NestJS teams that hit real throughput or correctness requirements make the same move shown here, usually via a distinct &lt;code&gt;@MessagePattern&lt;/code&gt; microservice or a queue-specific worker deployment rather than the main HTTP app.&lt;/p&gt;

&lt;p&gt;So this isn't a Python-specific lesson, it's what happens once "a background thing riding along in the API process" needs to be reasoned about as infrastructure rather than as an implementation detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule of Shared Libraries
&lt;/h2&gt;

&lt;p&gt;Set up a monorepo whenever you need to share models/services/repositories. For example you can have an &lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service/api" rel="noopener noreferrer"&gt;&lt;code&gt;api&lt;/code&gt;&lt;/a&gt; and a &lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service/worker" rel="noopener noreferrer"&gt;&lt;code&gt;worker&lt;/code&gt;&lt;/a&gt; in the same monorepo. They are separately deployables, but they share one database and one set of SQLAlchemy models/repositories, defined once in &lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service/shared" rel="noopener noreferrer"&gt;&lt;code&gt;shared&lt;/code&gt;&lt;/a&gt; and imported by both.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.systemoverflow.com/learn/design-fundamentals/communication-patterns/idempotency-at-least-once-delivery-and-the-outbox-inbox-pattern" rel="noopener noreferrer"&gt;At Least Once Delivery&lt;/a&gt; Guarantee
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service" rel="noopener noreferrer"&gt;the linked example&lt;/a&gt; we have two separate hops, because they have very different guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hop 1: RabbitMQ → Worker → Postgres
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;At-least-once delivery comes from RabbitMQ fundamentals: durable queue + persistent messages + manual ack. Nothing is acked until it's fully processed, so a crash mid-flight just means redelivery to whoever reconnects.&lt;/li&gt;
&lt;li&gt;Idempotency comes from the domain itself, not a deduplication table: &lt;a href="https://github.com/kasir-barati/python/blob/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service/shared/src/shared/db/user/repository.py#L20-L27" rel="noopener noreferrer"&gt;&lt;code&gt;UserRepository.get_or_create&lt;/code&gt;&lt;/a&gt; is naturally idempotent because "does a user with this email exist" is a deterministic query independent of how many times you ask. That's "idempotent by construction", which is a lighter-weight cousin of the inbox pattern (a real inbox pattern would track message IDs in a deduplication table for handlers whose side effects aren't naturally idempotent. E.g. "increment balance by $10"). Here you don't need that machinery because the operation happens to collapse to the same result on replay.&lt;/li&gt;
&lt;li&gt;No outbox table either. A classic transactional outbox would write "email to publish" into a DB table in the same transaction as the user upsert, then a separate relay process drains that table into RabbitMQ/Redis. Instead, this worker does DB commit → Redis publish → RabbitMQ ack, non-transactionally, and covers the gap by not acking until both steps succeed. If it crashes after commit but before ack, whole process (DB write + Redis publish) reruns. That's why the &lt;a href="https://github.com/kasir-barati/python/tree/34b5d750e15c055ed25f8cd5848b2668cbdde615/tips/examples/rabbitmq-worker-service#important-note-about-db-transaction--order-of-actions" rel="noopener noreferrer"&gt;README's crash table&lt;/a&gt; shows the DB write as safe (idempotent) but explicitly flags Redis as possibly duplicated. The ack-gating is doing the job an outbox table would normally do, at the cost of allowing duplicate publishes &lt;strong&gt;instead of&lt;/strong&gt; guaranteeing exactly-once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: at-least-once + idempotent handler, achieved via ack ordering, not via outbox/inbox infrastructure, a genuinely solid and simple implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hop 2: Worker → Redis pub/sub → GraphQL Subscription
&lt;/h3&gt;

&lt;p&gt;This is the part that isn't gracefully handled, and your instinct is right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicates from hop 1 leak straight through. When the worker crashes after redis.publish but before &lt;code&gt;basic_ack&lt;/code&gt;, the redelivery causes a second publish of the same email to any subscriber.&lt;/li&gt;
&lt;li&gt;There's currently no way for a client to tell besides treating the email address as some sort of message ID, or sequence number, or more commonly known as idempotency key.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Could you add an idempotency key over the websocket?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, nothing about GraphQL subscriptions or graphql-ws prevents it, it's just app-level payload shape. You'd:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give each RabbitMQ message a stable ID, derive one deterministically (e.g. hash of the email, or client whom is initiating this whole pipeline can send one, or we could simply use the ID generated and returned by database engine) so that a crash-retry reproduces the same ID rather than minting a new one.&lt;/li&gt;
&lt;li&gt;Carry that ID through: RabbitMQ message → Redis publish payload (as JSON: {"id": ..., "email": ...} instead of a bare string) → the Subscription.queue_messages yield → the GraphQL client.&lt;/li&gt;
&lt;li&gt;The GraphQL client keeps a small set/LRU of recently-seen IDs and drops repeats — that's effectively an &lt;em&gt;inbox pattern&lt;/em&gt; implemented at the client, since the server-side pub/sub layer has no persistence to build a server-side inbox against.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Whether it's worth doing depends on how much a duplicate matters to your subscribers. Since the downstream effect here is "a user row exists", a duplicate push is currently harmless if the client is also just doing an upsert-style process. It only becomes a real problem if a client does something non-idempotent in response to the subscription event (e.g., "send a welcome email every time this fires").&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>node</category>
      <category>python</category>
      <category>systemdesign</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Coding Agents &amp; Workflows</title>
      <dc:creator>Mohammad Jawad (Kasir) Barati</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:58:03 +0000</pubDate>
      <link>https://dev.to/kasir-barati/coding-agents-workflows-22kc</link>
      <guid>https://dev.to/kasir-barati/coding-agents-workflows-22kc</guid>
      <description>&lt;p&gt;If you're working with coding agents like Claude Code, GitHub Copilot, or any other AI assistant, you've probably noticed something: they can generate code faster than you can review it. This creates a new set of challenges that traditional development workflows weren't designed to handle.&lt;/p&gt;

&lt;p&gt;Here is what I believe will help you. BTW before reading this post try to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone a large open-source project which you feel comfortable working with.&lt;/li&gt;
&lt;li&gt;Find a TODO or bug in the code.&lt;/li&gt;
&lt;li&gt;Ask your coding agent to fix it.&lt;/li&gt;
&lt;li&gt;Apply whatever principles you &lt;em&gt;currently&lt;/em&gt; have when working with a coding agent (whatever that looks like today).&lt;/li&gt;
&lt;li&gt;Save the results in a separate branch. If you like you can also track how much you spend on it (time and LLM bills).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep that branch around, once you've read the post, you'll go back and redo the same task with the principles below applied, so you can compare the two results yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know Your Tools, Know Yourself
&lt;/h2&gt;

&lt;p&gt;Your choice of tools depends on your experience level, and that choice shapes the whole workflow you're about to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seasoned pros&lt;/strong&gt;: tend to gravitate toward CLI-based tools. And let's not forget that most of the times CLI offers more features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beginners&lt;/strong&gt;: often find IDEs more comfortable and approachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever you pick, you need to learn to be patient with LLMs. They usually tend to generate a ton of text, and rushing past it defeats the point of reviewing it at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Give Your Agent Context, Deliberately
&lt;/h2&gt;

&lt;p&gt;When you code, you have a ton of context in your head. So if you are expecting a coding agent to deliver good results you need to give it &lt;strong&gt;enough&lt;/strong&gt; context but beware of &lt;a href="https://diffray.ai/blog/context-dilution/" rel="noopener noreferrer"&gt;context dilution&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Browse official plugins.&lt;/li&gt;
&lt;li&gt;Pick the most popular or relevant ones for your project type.&lt;/li&gt;
&lt;li&gt;Examples:

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://claude.com/plugins/feature-dev" rel="noopener noreferrer"&gt;feature-dev&lt;/a&gt; for building features efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.com/plugins/code-simplifier" rel="noopener noreferrer"&gt;code-simplifier&lt;/a&gt; to clean up and refactor.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.com/plugins/frontend-design" rel="noopener noreferrer"&gt;frontend-design&lt;/a&gt; for professional UI/UX work.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you install &lt;a href="https://claude.com/plugins/superpowers" rel="noopener noreferrer"&gt;superpowers&lt;/a&gt; plugin get ready for it to spin up subagents and burn a ton of tokens in the process. Personally I downloaded their brainstorming, and systematic-debugging skill and removed the plugin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  MCP Servers Worth Knowing About
&lt;/h3&gt;

&lt;p&gt;Consider using &lt;strong&gt;MCP servers&lt;/strong&gt; (like &lt;a href="https://context7mcp.com/claude/" rel="noopener noreferrer"&gt;Contact7&lt;/a&gt;) if they offer exactly what you need. Some plugins may already cover the same functionality, so try to avoid overlap. Commit them to git so everyone has access.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install Context7, it is so useful since LLMs do not have always the latest documentations for a library. You can enforce it by adding a &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;/p&gt;


&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context7"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@upstash/context7-mcp@latest"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Case Study: Why Skills Matter
&lt;/h3&gt;

&lt;p&gt;Rather than relying on general knowledge, develop skills tailored to your project. Imagine you are building the backend checkout service for a massive e-commerce platform (think Shopify, Amazon, or a large retail enterprise). This service handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cart finalization with 10+ items.&lt;/li&gt;
&lt;li&gt;Inventory reservation (not just decrementing a number, but holding stock across multiple warehouses).&lt;/li&gt;
&lt;li&gt;Tax calculation (different rules per US state, VAT for EU, GST for Australia).&lt;/li&gt;
&lt;li&gt;Payment orchestration (Stripe/Paddle/Braintree with 3D Secure fallbacks).&lt;/li&gt;
&lt;li&gt;Fraud scoring (an internal ML model that returns a risk score).&lt;/li&gt;
&lt;li&gt;Promotions &amp;amp; gift cards (stacking rules).&lt;/li&gt;
&lt;li&gt;Event emission (RabbitMQ events for shipping, analytics, and receipts).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now an engineer on your team asks the coding agent to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We are releasing a 'Flash Sale' feature tomorrow. We need a new endpoint that accepts a list of product IDs and quantities, bypasses the user's shopping cart, and goes straight to the checkout/payment page. Ensure it validates the flash sale time window."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What happens WITHOUT a skill:&lt;/strong&gt; the agent reads the prompt, sees &lt;strong&gt;"bypass cart"&lt;/strong&gt; and &lt;strong&gt;"straight to payment"&lt;/strong&gt;, and happily writes something like this (LLMs try to follow your instructions as much as possible):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/flash-checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flash_checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# 🚨 UNSAFE: Direct DB hit.
&lt;/span&gt;        &lt;span class="c1"&gt;# Inventory is immediately deducted, even if the user's credit card fails (loss of stock).
&lt;/span&gt;        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_one&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$inc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt; 
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;

    &lt;span class="c1"&gt;# 🚨 WRONG: Hardcodes US tax, ignores EU and other regions
&lt;/span&gt;    &lt;span class="c1"&gt;# This is a contrived example and most top LLMs would ask you about it
&lt;/span&gt;    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;1.08&lt;/span&gt;

    &lt;span class="c1"&gt;# 🚨 BREAKS STATE MACHINE: Direct status update
&lt;/span&gt;    &lt;span class="c1"&gt;# State machine is broken, so the shipping service never picks it up.
&lt;/span&gt;    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_one&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# 🚨 NO IDEMPOTENCY: User double-clicking charges them twice
&lt;/span&gt;    &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine what would have happened if you had a skill like this in &lt;code&gt;.claude/skills/checkout-orchestration-skill/SKILL.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-orchestration-skill&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Enforces&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;distributed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;saga&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pattern&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;e-commerce&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;checkout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pipeline.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;skill&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;whenever&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;implementing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modifying&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;placement,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;payment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;flows,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cart&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;finalization.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;It&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mandates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;idempotency&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Redis),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inventory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reservation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(never&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;direct&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deduction),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;machine&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transitions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(OrderStateMachine),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;aggregation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(TaxJar/Vertex&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;routing),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fraud&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;handling&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(800ms&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;timeout),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Kafka&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;emission&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(order_placed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;topic)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;eventual&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;consistency.'&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Core Architectural Rules&lt;/span&gt;

&lt;span class="gs"&gt;**NEVER**&lt;/span&gt; treat checkout as a single CRUD update. It is a &lt;span class="gs"&gt;**distributed saga**&lt;/span&gt;.
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Idempotency First**&lt;/span&gt;: Every checkout request must include an &lt;span class="sb"&gt;`idempotency_key`&lt;/span&gt;. If the key exists in Redis, return the cached result immediately. 
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Reserve, Do Not Deduct**&lt;/span&gt;: Never write directly to &lt;span class="sb"&gt;`inventory`&lt;/span&gt; table. Always call &lt;span class="sb"&gt;`InventoryReservationService.reserve()`&lt;/span&gt; which holds stock for 15 minutes (timeout period). Only finalize deduction after &lt;span class="sb"&gt;`PAYMENT_CONFIRMED`&lt;/span&gt; event.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**State Machine Only**&lt;/span&gt;: The &lt;span class="sb"&gt;`Order`&lt;/span&gt; entity has a strict state machine: &lt;span class="sb"&gt;`PENDING -&amp;gt; FRAUD_CHECK -&amp;gt; AUTHORIZED -&amp;gt; PAYMENT_CAPTURED -&amp;gt; FULFILLMENT_QUEUE`&lt;/span&gt; Never skip states. Never manually set &lt;span class="sb"&gt;`order.status = 'completed'`&lt;/span&gt;. Use &lt;span class="sb"&gt;`OrderStateMachine.transition('CAPTURE')`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Tax Agnosticism**&lt;/span&gt;: We use TaxJar for US/CA, and Vertex for EU. Always call &lt;span class="sb"&gt;`TaxAggregator.get_tax()`&lt;/span&gt; which handles the routing under the hood. Do NOT hardcode tax rates.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Fraud Timeout**&lt;/span&gt;: The fraud check is async. If it takes &amp;gt; 800ms, we proceed with &lt;span class="sb"&gt;`FRAUD_PENDING`&lt;/span&gt; status and place a dead-letter retry, but we do NOT block the user.
&lt;span class="p"&gt;6.&lt;/span&gt; &lt;span class="gs"&gt;**Eventual Consistency**&lt;/span&gt;: After capturing payment, push exactly one event to the &lt;span class="sb"&gt;`order_placed`&lt;/span&gt; Kafka topic. Must include the &lt;span class="sb"&gt;`correlation_id`&lt;/span&gt; for tracing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now if you send the same prompt to the coding agent, it generates real, production-grade, domain-aware code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/flash-checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flash_checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Enforce Idempotency
&lt;/span&gt;    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Validate Flash Sale window (domain logic)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flash_sale_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Flash sale not active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Build line items
&lt;/span&gt;    &lt;span class="n"&gt;line_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Reserve inventory using the domain service (DOES NOT deduct yet)
&lt;/span&gt;    &lt;span class="n"&gt;reservation_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InventoryReservationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve_bulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 5. Get tax through the aggregator (handles US/EU routing)
&lt;/span&gt;    &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TaxAggregator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 6. Initiate state machine
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStateMachine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 7. Fraud check (async, non-blocking)
&lt;/span&gt;    &lt;span class="n"&gt;fraud_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FraudScorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 8. Authorization (not capture, just auth to check funds)
&lt;/span&gt;    &lt;span class="n"&gt;auth_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 9. Transition to authorized state
&lt;/span&gt;    &lt;span class="n"&gt;OrderStateMachine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTHORIZE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# 10. Emit Kafka event for downstream processing
&lt;/span&gt;    &lt;span class="n"&gt;event_bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_placed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlation_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Correlation-ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTHORIZED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skills are part of the &lt;a href="https://paelladoc.com/blog/harness-engineering" rel="noopener noreferrer"&gt;harness engineering&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Commands
&lt;/h3&gt;

&lt;p&gt;You can e.g. create a command for labeling GitHub issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/commands
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .claude/commands/label-github-issue.md &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
---
description: Fetch a GitHub issue and apply appropriate labels using gh
argument-hint: &amp;lt;github-issue-url&amp;gt;
allowed-tools: Bash(gh issue view:*), Bash(gh label list:*), Bash(gh issue edit:*), Bash(gh repo view:*)
---

You are labeling a GitHub issue. The issue URL is: &lt;/span&gt;&lt;span class="nv"&gt;$ARGUMENTS&lt;/span&gt;&lt;span class="sh"&gt;

Steps:
1. Parse the URL to extract the owner, repo, and issue number.
2. Run `gh issue view &amp;lt;number&amp;gt; --repo &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; --json title,body,labels` to read the issue's current title, body, and existing labels.
3. Run `gh label list --repo &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;` to see which labels actually exist in this repo. Only use labels from this list, if you strongly believe we lack certain label just let the user know as a side note, do NOT stop here even if you believe certain labels are missing.
4. Based on the issue's title and body, decide which existing labels best apply (e.g. bug, enhancement, documentation, question, good first issue, priority levels, area/* labels, etc).
5. Apply the chosen labels with `gh issue edit &amp;lt;number&amp;gt; --repo &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; --add-label "label1,label2"`.
6. Report back to the user: which labels you applied and a one-line reason for each. Also report back the labels which you believe are good to add with a single line as to why.

If the issue already has labels that are still appropriate, leave them and only add what's missing. If no labels in the repo genuinely fit, say so instead of forcing one on.
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And ensure you are committing them so others will be using the same commands when needed!&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;AGENTS.md&lt;/code&gt;: Your Agent's Navigation System
&lt;/h3&gt;

&lt;p&gt;The most important investment you can make is in documenting a navigation system for your AI agents. You wanna commit and push this documentation to your VCS. And I believe you already know it but I usually just link to &lt;code&gt;AGENTS.md&lt;/code&gt; in &lt;code&gt;CLAUDE.md&lt;/code&gt;. This way I do not have to copypaste or maintain both.&lt;/p&gt;

&lt;p&gt;Also you can have a &lt;code&gt;AGENTS.local.md&lt;/code&gt;/&lt;code&gt;CLAUDE.local.md&lt;/code&gt; for your local setup which is not committed to git. So in &lt;code&gt;AGENTS.md&lt;/code&gt; we usually put stuff such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash commands.&lt;/li&gt;
&lt;li&gt;Common MCP tools.&lt;/li&gt;
&lt;li&gt;Style guides, this can be:

&lt;ul&gt;
&lt;li&gt;Frontend UX.&lt;/li&gt;
&lt;li&gt;Best practices and design patterns.&lt;/li&gt;
&lt;li&gt;Testing strategies (although you can move this to a separate markdown file and just link it).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Architectural decisions.&lt;/li&gt;
&lt;li&gt;A link to important files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically anything you usually need to work on that codebase. But make sure to keep it short and concise since if it is too long then it just take up space in your context window with no real benefits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❗ &lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can have a &lt;code&gt;AGENTS.md&lt;/code&gt;/&lt;code&gt;CLAUDE.md&lt;/code&gt; in subdirectories of a project, and coding agents load them when reading and working with that directory's files. So there you can be more meticulous with your instructions.&lt;/p&gt;

&lt;p&gt;I would also like to make it crystal clear that LLMs love to optimize for coverage percentages. They'll often write brittle tests that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overly mock dependencies.&lt;/li&gt;
&lt;li&gt;Test every code path without regard to actual functionality.&lt;/li&gt;
&lt;li&gt;Break when you refactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what we want from our test suite cases are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good tests&lt;/strong&gt;: Test the functionality you're building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring-safe&lt;/strong&gt;: Shouldn't break when you reimplement. NOTE, we are assuming the APIs remain the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic-catching&lt;/strong&gt;: Should break when logic breaks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Process Discipline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Embrace Trial and Error
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Adopt an &lt;strong&gt;experimenter's mindset&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Start a task, see how it goes.&lt;/li&gt;
&lt;li&gt;If it doesn't work, abandon it and try another approach.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;This flexibility is critical, don't be afraid to throw away what doesn't serve you.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Manage Context Proactively
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check how much of the context is filled, e.g. in Cloud Code, you must use &lt;code&gt;/context&lt;/code&gt; frequently.&lt;/li&gt;
&lt;li&gt;Don't always wait for coding agent to compact the context. You can do it yourself too if for example you need a fresh start and just wanted to summarize the current state in a more controlled manner.&lt;/li&gt;
&lt;li&gt;Clear your context with &lt;code&gt;/clear&lt;/code&gt; and start fresh when needed.&lt;/li&gt;
&lt;li&gt;Write summaries to markdown to preserve important info before clearing. This is where a &lt;code&gt;PLAN.md&lt;/code&gt; or similar progress log earns its keep: treat it as your project memory and communication log, not just a one-off note.&lt;/li&gt;
&lt;li&gt;Work in bite-sized chunks. So instead of:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Hey agent, refactor this 50-person project's entire codebase.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to ask LLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Break this large task into 10 small, specific steps. Where each step should be independently:
  - Specifiable.
  - Testable.
  - Reviewable by a human.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Explore, Plan, Confirm, Then Code
&lt;/h3&gt;

&lt;p&gt;It is a good practice to start with writing a &lt;code&gt;PLAN.md&lt;/code&gt; to brainstorm what you wanna do. In fact, that's why I downloaded &lt;a href="https://github.com/obra/superpowers/tree/b36e0829c6d0140e93cfef2ca599b1b07d4a7797/skills/brainstorming" rel="noopener noreferrer"&gt;the brainstorming skill from the superpowers plugin&lt;/a&gt;. So next time you wanna develop a feature which is big enough for the LLM to derail or misunderstand how it should work, first ask it to brainstorm and write a plan for you. Review and work on that plan, then start with implementing it. So if I wanted to visualize this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Explore] =&amp;gt; [Plan] =&amp;gt; [Confirm] =&amp;gt; [Code] =&amp;gt; [Commit]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Figure out the root cause for issue #983, then propose a few fixes. Let me choose an approach before you code. ultrathink&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Pro Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"ultrathink" is a special "magic keyword" you can add anywhere in your prompt to trigger a maximum reasoning depth mode for that specific request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Give It Acceptance Criteria
&lt;/h3&gt;

&lt;p&gt;If you give your coding agent a way to measure how good it did, the results would be closer to what you wanted since it will iterate over it. For this usually we can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write tests =&amp;gt; commit =&amp;gt; code =&amp;gt; iterate =&amp;gt; code:
&amp;gt; Write tests for @utils/markdown.ts to make sure links render properly (note the tests won't pass yet, since links aren't yet implemented). Then commit. Then update the code to make the tests pass.&lt;/li&gt;
&lt;li&gt;Write code =&amp;gt; screenshot results =&amp;gt; iterate:
&amp;gt; Implement [mock.png]. Then screenshot it with Puppeteer and iterate until it looks like the mock.&lt;/li&gt;
&lt;li&gt;Write acceptance criteria in a markdown files similar to what you usually get in a Jira ticket written by a product owner:
&amp;gt; Take @specs/payment-flow.md as the source of truth. Write e2e integration tests that mirror each acceptance scenario exactly. Run the tests, and they should fail initially. Then implement the @api/payment/ module and keep iterating until every test passes. Let me know which ACs were tricky or ambiguous.
Or you can e.g. write
&amp;gt; Let's work on &lt;a href="https://acmecorp.atlassian.net/browse/PAY-842" rel="noopener noreferrer"&gt;https://acmecorp.atlassian.net/browse/PAY-842&lt;/a&gt;. Treat each bullet point in acceptance criteria section as a hard pass/fail criterion. Build the @app/checkout feature, then simulate a full user journey through the UI (using Playwright) and check each block. If a step fails, pause, fix, and re-run until the entire ticket is green. Do not mark the ticket as done until all checks pass.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Ownership &amp;amp; the Safety Net
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use Git Heavily
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Git is your safety net.&lt;/li&gt;
&lt;li&gt;Commit often.&lt;/li&gt;
&lt;li&gt;Branch freely.&lt;/li&gt;
&lt;li&gt;Roll back with confidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  You Own Every Line You Push
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No AI slop&lt;/strong&gt;, look out for:

&lt;ul&gt;
&lt;li&gt;Long, rambling files that are hard to review.&lt;/li&gt;
&lt;li&gt;Overly defensive code (excessive error handling, validation).&lt;/li&gt;
&lt;li&gt;Generated code that's "technically correct" but architecturally poor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Avoid:

&lt;ul&gt;
&lt;li&gt;Unnecessary tests.&lt;/li&gt;
&lt;li&gt;Extra READMEs.&lt;/li&gt;
&lt;li&gt;Emojis.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Be ruthless about code quality.&lt;/li&gt;
&lt;li&gt;Ensure everything you push is clean, purposeful, and well-written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember I have already talked about it &lt;a href="https://dev.to/kasir-barati/pragmatic-agentic-programmer-994"&gt;in my post on pragmatic agentic programming&lt;/a&gt;. This is code you stand behind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a culture of rejecting mediocre AI-generated code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bonus: Use Your Agent to Onboard, Not Just to Code
&lt;/h2&gt;

&lt;p&gt;It is always a good idea to have another engineer to onboard you. But nowadays you can simply try to utilize LLMs and coding agents to help you with that as well. Ask questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How is @RoutingController.py used?&lt;/li&gt;
&lt;li&gt;How do I make a new @app/services/ValidationTemplateFactory?&lt;/li&gt;
&lt;li&gt;Why does &lt;code&gt;recoverFromException&lt;/code&gt; take so many arguments? Look through git history to answer.&lt;/li&gt;
&lt;li&gt;I'm new to the codebase. Give me a mental model of the &lt;a class="mentioned-user" href="https://dev.to/core"&gt;@core&lt;/a&gt;/ folder, what are the primary responsibilities of each subdirectory, and which one should I touch if I need to modify the logging behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or you can ask other questions outside of onboarding process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did we fix issue #18363 by adding the &lt;code&gt;if/else&lt;/code&gt; in &lt;a class="mentioned-user" href="https://dev.to/src"&gt;@src&lt;/a&gt;/login.ts API?&lt;/li&gt;
&lt;li&gt;In which version did we release the new @api/ext/PreHooks.php API?&lt;/li&gt;
&lt;li&gt;Look at PR #9383, then carefully verify which app versions were impacted.&lt;/li&gt;
&lt;li&gt;What did I ship last week?&lt;/li&gt;
&lt;li&gt;If I deprecate the @utils/legacyParser.ts function, which modules across the entire monorepo are still relying on it? Show me the call stack in each case.&lt;/li&gt;
&lt;li&gt;Why was the @api/middleware/AuthGuard implemented as a class instead of a factory function? Look at the original PR and the team's discussion history to explain the trade-offs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your coding agent, if it is smart enough, will be able to look at git history whenever needed, and let's imagine you have an MCP server to return your microservice architecture. Then it will use it to gain a deeper understanding of how service A interacts with service B. Sometimes you have to be specific so it knows it must use the MCP server. And there are times it can figure that out itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Asymmetry Problem
&lt;/h2&gt;

&lt;p&gt;It's becoming increasingly easy to generate tons of code, and the burden is shifting to humans to review it all. Everything above is really one answer to this same problem, combat it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disciplined review processes.&lt;/li&gt;
&lt;li&gt;Challenging agents to write succinct, clean code.&lt;/li&gt;
&lt;li&gt;Making sure agents don't overwhelm reviewers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now go back to that first branch you made. Apply what you've read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write a detailed &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use plugins where they fit.&lt;/li&gt;
&lt;li&gt;Break the work into small, reviewable steps.&lt;/li&gt;
&lt;li&gt;Review the output critically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the results in a separate branch, and compare it against your first attempt. That difference is the whole point of this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/live/6eBSHbLKuN0?si=Yy7r9-5OFapkoXuJ" rel="noopener noreferrer"&gt;Mastering Claude Code in 30 minutes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>programming</category>
      <category>productivity</category>
      <category>workflow</category>
    </item>
  </channel>
</rss>
