<?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>Forem: Vitalii Cherepanov</title>
    <description>The latest articles on Forem by Vitalii Cherepanov (@vbcherepanov).</description>
    <link>https://forem.com/vbcherepanov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890596%2Fff25135b-34bf-4dca-b53c-5be301b41ea5.jpg</url>
      <title>Forem: Vitalii Cherepanov</title>
      <link>https://forem.com/vbcherepanov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/vbcherepanov"/>
    <language>en</language>
    <item>
      <title>RAG isn't memory. It's Ctrl+F with embeddings.</title>
      <dc:creator>Vitalii Cherepanov</dc:creator>
      <pubDate>Fri, 01 May 2026 12:12:41 +0000</pubDate>
      <link>https://forem.com/vbcherepanov/rag-isnt-memory-its-ctrlf-with-embeddings-1imi</link>
      <guid>https://forem.com/vbcherepanov/rag-isnt-memory-its-ctrlf-with-embeddings-1imi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 1 of 3 — "Memory for AI agents"&lt;/strong&gt;&lt;br&gt;
Deconstructing the long-term memory myth in LLM systems&lt;/p&gt;
&lt;/blockquote&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.amazonaws.com%2Fuploads%2Farticles%2Frp9xzcea3fsbcunjbvuu.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.amazonaws.com%2Fuploads%2Farticles%2Frp9xzcea3fsbcunjbvuu.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Article
&lt;/h2&gt;

&lt;p&gt;It's 3 AM. I'm on my third night debugging an AI agent. I'm standing in the kitchen with a mug of tea, staring at a diff, swearing quietly. The agent has confidently rewritten the auth function — based on a chunk that belongs to a branch that was deleted from the repo two months ago.&lt;/p&gt;

&lt;p&gt;The chunk lives in Qdrant. Its cosine similarity to my query is high. Top-1 in the retrieval. The agent honestly grabbed it, honestly stitched it into the prompt, honestly generated the "correct" patch. Against code from a different reality.&lt;/p&gt;

&lt;p&gt;I close the laptop and think: okay, I have RAG. I have vectors. I have long-term memory. I have everything every AI conference deck has been promising for the last two years. Why did my agent just propose a fix based on code that doesn't exist anymore?&lt;/p&gt;

&lt;p&gt;Because my agent doesn't have memory. My agent has search results with cosine instead of BM25. And between those two sentences lies the entire difference between &lt;em&gt;"AI you can trust in production"&lt;/em&gt; and &lt;em&gt;"AI you have to babysit on every line."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This piece is about that difference. And about why we, as engineers, are the ones to blame for not seeing it anymore.&lt;/p&gt;




&lt;h3&gt;
  
  
  The devaluation of the word "memory"
&lt;/h3&gt;

&lt;p&gt;Let's be honest. What is the typical "memory" of an AI agent in 2026?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text → split into 512-1024 token chunks
     → embedding (bge / text-embedding-3 / openai)
     → vector DB (Qdrant / pgvector / Chroma / Pinecone)
     → cosine similarity top-k
     → concatenate into prompt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; memory. This is search. It's old-school Lucene from 2003, repainted in neural colors. Cosine instead of TF-IDF. Embeddings instead of an inverted index. Same thing.&lt;/p&gt;

&lt;p&gt;If we just called it that — &lt;em&gt;"vector search,"&lt;/em&gt; &lt;em&gt;"semantic retrieval"&lt;/em&gt; — I'd have no complaints. Call Lucene Lucene, no problem. But when it's sold under the banner &lt;em&gt;"my AI has long-term memory"&lt;/em&gt; — sorry. My AI has déjà vu and amnesia at the same time.&lt;/p&gt;

&lt;p&gt;This isn't a terminology gripe. It's a question of expectations. When an engineer hears &lt;em&gt;"memory,"&lt;/em&gt; they imagine a system that &lt;strong&gt;remembers&lt;/strong&gt;: who said what, when, in what context, what was true then versus what's true now. When an engineer gets RAG, they get Ctrl+F. And instead of building honest architecture around that Ctrl+F — with honest constraints — they build a sandcastle and wonder why the agent confuses past with present.&lt;/p&gt;




&lt;h3&gt;
  
  
  Three holes you can drive a truck through
&lt;/h3&gt;

&lt;p&gt;Three concrete failures. Each one I caught in production. Not theory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hole #1: A chunk doesn't know it's a chunk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take a perfectly normal declaration from a design doc:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"We moved to JWT because opaque sessions didn't scale to our traffic profile. The alternative was stateful sessions with a Redis cluster, but we ruled it out because of audit requirements from a customer — they don't allow session state outside their perimeter. JWT solves both, but adds invalidation complexity, which we mitigate with short TTLs and refresh tokens."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The chunker splits this into four 512-token pieces. On retrieval, a query comes in: &lt;em&gt;"why did we pick JWT?"&lt;/em&gt; Top-3 returns three fragments of the same decision. With no causality. Without the alternative we ruled out. Without the trade-off we accepted.&lt;/p&gt;

&lt;p&gt;A decision that was &lt;strong&gt;whole&lt;/strong&gt; turns into three parallel "factoids." The model honestly stitches them into plausible text — and &lt;strong&gt;invents&lt;/strong&gt; the missing connections. Because its job is to generate plausible text. And it will, without blinking.&lt;/p&gt;

&lt;p&gt;This isn't a bug in the chunker. This is an architectural property of the entire approach. Any decision declaration you have gets ground into powder and reassembled with structural loss. Every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hole #2: There's no structure in memory. Only cosine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a human explains a project to you, they say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;here's the goal&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;here are the options we considered&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;here's what we picked and why&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;here's what broke two months later&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;here's what we changed, and that decision now supersedes the old one&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In RAG, none of this exists. Zero. RAG doesn't distinguish &lt;em&gt;"hypothesis,"&lt;/em&gt; &lt;em&gt;"confirmed fact,"&lt;/em&gt; &lt;em&gt;"rejected alternative,"&lt;/em&gt; &lt;em&gt;"deprecated decision moved to archive."&lt;/em&gt; For RAG, all of these are equivalent points in a 384-dimensional space.&lt;/p&gt;

&lt;p&gt;Imagine you're trying to record thirty years of life into a single flat table &lt;code&gt;entries(text, vector)&lt;/code&gt; and then search it by cosine. Surprised your memories blur together? That's not your memory failing. That's the structure you crammed it into — a structure that doesn't allow distinctions between &lt;em&gt;"I thought about it"&lt;/em&gt; and &lt;em&gt;"I did it,"&lt;/em&gt; between &lt;em&gt;"I tried it and it worked"&lt;/em&gt; and &lt;em&gt;"I tried it and it hurt."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In RAG, there are no fields for these distinctions. Not because the developers didn't think of it. Because &lt;strong&gt;the vector-plus-distance paradigm itself&lt;/strong&gt; doesn't accommodate causality and time. It's a mathematical limitation. You don't fix it with product features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hole #3: Time doesn't exist as a first-class concept.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three weeks ago I wrote into the agent's memory: &lt;em&gt;"we use Postgres."&lt;/em&gt; Today I wrote: &lt;em&gt;"we migrated to ClickHouse for analytics, Postgres is OLTP only now."&lt;/em&gt; In RAG, &lt;strong&gt;both&lt;/strong&gt; facts sit there. Both have high cosine to a database query. Top-k returns both. The model picks the one that "sounds" better in its pretraining — usually Postgres, because it appears more often in the training data.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; memory. This is a roulette wheel disguised as confidence.&lt;/p&gt;

&lt;p&gt;When was the last time you saw &lt;code&gt;valid_from&lt;/code&gt;, &lt;code&gt;valid_until&lt;/code&gt;, &lt;code&gt;deprecated_by&lt;/code&gt;, &lt;code&gt;replaced_by&lt;/code&gt;, &lt;code&gt;superseded_by&lt;/code&gt; fields in a production RAG system? I never have. Because in standard RAG, they're &lt;strong&gt;not in the schema&lt;/strong&gt;. And again — not because devs are lazy. Because the schema &lt;em&gt;"text plus embedding"&lt;/em&gt; has no place for the lifecycle of knowledge. No notion of &lt;em&gt;"this is true now"&lt;/em&gt; versus &lt;em&gt;"this was true then."&lt;/em&gt; Everything collapses into a single time slice — a present that somehow contains yesterday, last year, and deprecated-three-quarters-ago all at once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ctrl+F with embeddings doesn't &lt;strong&gt;remember&lt;/strong&gt;. It &lt;strong&gt;finds&lt;/strong&gt;. Different verbs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  "But memory frameworks fix this, right?"
&lt;/h3&gt;

&lt;p&gt;Okay, the believer says. There's mem0, Letta, Zep, Cognee, MemGPT, the whole long-term memory zoo. They added a meaning layer on top of RAG. They're memory-aware.&lt;/p&gt;

&lt;p&gt;Let's be honest. I've used them. One after another. For a long time. Looked under the hood, not just at the landing pages.&lt;/p&gt;

&lt;p&gt;Each of them takes &lt;strong&gt;one&lt;/strong&gt; piece of real memory — for some it's LLM-extraction before write, for some it's a buffer hierarchy like an OS, for some it's post-hoc graph extraction from dialogues, for some it's per-fact temporal validity — and implements &lt;strong&gt;that one piece&lt;/strong&gt;, without weaving it into the rest.&lt;/p&gt;

&lt;p&gt;This is warmer than vanilla Qdrant. It's &lt;strong&gt;not&lt;/strong&gt; a solution.&lt;/p&gt;

&lt;p&gt;Because real memory requires &lt;strong&gt;seven&lt;/strong&gt; properties working together. Each of them, in isolation, already exists in the literature or in open source. As far as I can tell, no one has assembled all seven into a single system. Which seven, exactly — that's part 2 of this series. Here, only the limitation that unites &lt;strong&gt;all&lt;/strong&gt; flat-fact solutions, however they wrap themselves:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of them have the right to say "I don't know."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Show me any one of these systems with a formal abstain mechanism: a gate through which a fact will &lt;strong&gt;not&lt;/strong&gt; pass into prompt context if it has no source, no confidence, no temporal validity, or an unresolved contradiction. I'll wait.&lt;/p&gt;

&lt;p&gt;In the standard flow of all these frameworks, the system's response to &lt;em&gt;"there's a contradiction in memory or not enough data"&lt;/em&gt; is &lt;em&gt;"well, the model will figure it out."&lt;/em&gt; Which translates from marketing to engineering as &lt;em&gt;"the model will hallucinate, and that becomes your problem in production."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good memory isn't &lt;em&gt;"remembering a lot."&lt;/em&gt; It's &lt;strong&gt;knowing the boundary of what you don't remember&lt;/strong&gt;. Part 2 of this series is built around that thesis.&lt;/p&gt;




&lt;h3&gt;
  
  
  "Why not just push context to 1M tokens?"
&lt;/h3&gt;

&lt;p&gt;This is the second fashion of the last two years, and it deserves its own breakdown, because it leads the industry into the same dead end under a different banner. &lt;em&gt;"Why do we need memory if Gemini has 2M context, Claude has 1M?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Four problems, no preamble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One — economics.&lt;/strong&gt; A single project conversation at 800K tokens with prompt caching off costs tens of dollars &lt;strong&gt;per request&lt;/strong&gt;. Without aggressive caching, you're broke in a week. With aggressive caching, you're building exactly the same hierarchy as Letta — just more expensive and locked to one vendor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two — recall.&lt;/strong&gt; Every long-context benchmark (NIH, Ruler, LongMemEval) shows the same thing: models &lt;strong&gt;drown&lt;/strong&gt; in their own context past 200-300K tokens. Attention is unevenly distributed. This is &lt;strong&gt;lost-in-the-middle&lt;/strong&gt;, and it doesn't get fixed by window size — it gets partially mitigated by architectural tricks inside the model, but it doesn't go away. The more you stuff in, the less of it actually gets considered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three — persistence.&lt;/strong&gt; Context isn't saved. Close the session, gone. Tomorrow the same agent shows up with a clean context. So you have to feed it 800K tokens of "history" again. The problem isn't solved — it's hidden inside your wallet and your latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four — learning.&lt;/strong&gt; If the agent made a mistake yesterday and you corrected it, that experience isn't structured for the future. Tomorrow it'll repeat the mistake. Context is RAM, not disk. And when someone says &lt;em&gt;"just increase context instead of building memory"&lt;/em&gt; — that's the same as saying &lt;em&gt;"why do I need a database, I have a terabyte of RAM."&lt;/em&gt; Technically the words rhyme. In practice they're incomparable concepts.&lt;/p&gt;

&lt;p&gt;Big context doesn't replace memory. It lets you stuff more into one session — and that's it.&lt;/p&gt;




&lt;h3&gt;
  
  
  What to do about it tomorrow morning
&lt;/h3&gt;

&lt;p&gt;If you've read this far and you're thinking &lt;em&gt;"okay, agreed, RAG is search, not memory. Now what?"&lt;/em&gt; — I have two pieces of news.&lt;/p&gt;

&lt;p&gt;The bad: a systemically correct solution requires rewriting the memory layer from schema up through lifecycle, and that's months of work. Not a weekend.&lt;/p&gt;

&lt;p&gt;The good: there are several things you can do &lt;strong&gt;tomorrow morning&lt;/strong&gt; that already remove half the pain. Not magic — just engineering hygiene.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drop the word "memory" from your stack if what you have is RAG.&lt;/strong&gt; Call it retrieval or search — instantly more honest. That alone removes 80% of inflated expectations from users and the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduce &lt;code&gt;valid_from&lt;/code&gt; and &lt;code&gt;valid_until&lt;/code&gt; for every fact.&lt;/strong&gt; Any fact without temporal validity is a hypothesis, not a fact. Old facts should drop out of retrieval automatically, not compete with new ones on cosine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distinguish &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;working&lt;/code&gt;, &lt;code&gt;consolidated&lt;/code&gt;, &lt;code&gt;archived&lt;/code&gt;.&lt;/strong&gt; Don't dump everything into one collection. A fact that just arrived and a piece of knowledge confirmed by tests are different entities with different weight in retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make abstain a first-class outcome.&lt;/strong&gt; If no fact passed the confidence threshold during retrieve, the system &lt;strong&gt;must&lt;/strong&gt; have the right to say &lt;em&gt;"I don't know, I need data."&lt;/em&gt; And that &lt;em&gt;"I don't know"&lt;/em&gt; should become a task in the backlog, not a dead end for the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a complete list — it's the minimum to start the transition from &lt;em&gt;"I have RAG, I call it memory"&lt;/em&gt; to &lt;em&gt;"I have memory, and it knows its boundaries."&lt;/em&gt; The full list of seven principles is in part 2.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where this comes from
&lt;/h3&gt;

&lt;p&gt;I sit deep in this kitchen — Claude Code, Cursor, Codex, Windsurf, MCP servers, mem0, Zep, local RAG stacks on Postgres + pgvector, Qdrant, Chroma. Over the last few months I've tried, I think, everything on the market. I have my own MCP memory server with about fifteen hundred entries, which I rewrote from scratch three times because each time I hit one of the three holes above.&lt;/p&gt;

&lt;p&gt;At some point, I got tired. Not of AI — of what we call memory at AI. Sat down and started writing my own cognitive runtime that &lt;strong&gt;doesn't pretend to know&lt;/strong&gt;, that &lt;strong&gt;knows what it doesn't know&lt;/strong&gt;, and that &lt;strong&gt;sets its own tasks&lt;/strong&gt; to close the gaps. Called it &lt;code&gt;braincore&lt;/code&gt;. One Go binary, local, MCP-stdio, Apache-2.0. Not a pitch, because it's open source — just an example that I say &lt;em&gt;"this can be done"&lt;/em&gt; not theoretically.&lt;/p&gt;

&lt;p&gt;Seven architectural principles it's built on — that's part 2 of this series. Drops in a week. I'll cover atomic knowledge units, lifecycle, strict mode, causal decision chains, AST-based identity for code, internal git as memory versioning, memory scoring, and negative memory.&lt;/p&gt;

&lt;p&gt;And why all of that combined produces a qualitatively different result than any of those pieces in isolation.&lt;/p&gt;

&lt;p&gt;Part 3 is philosophical — about &lt;strong&gt;the right of an AI agent to stay silent&lt;/strong&gt;, and why the right metric for production AI isn't accuracy but &lt;em&gt;zero confidently-wrong actions at an acceptable abstain rate&lt;/em&gt;. About self-tasking. About why cognitive runtime matters more than model size.&lt;/p&gt;




&lt;p&gt;If you read this far and recognized yourself in the opening paragraph — we're in the same boat. If you have RAG that you call memory and it works — tell me how, seriously, I want to know, I might be wrong.&lt;/p&gt;

&lt;p&gt;The one thing you can't do is stay silent.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1 of 3. Next — "Seven principles of real memory for AI agents" — drops next Tuesday.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>I Studied the etcd Codebase — and It Changed How I Write PHP</title>
      <dc:creator>Vitalii Cherepanov</dc:creator>
      <pubDate>Tue, 21 Apr 2026 10:41:13 +0000</pubDate>
      <link>https://forem.com/vbcherepanov/i-studied-the-etcd-codebase-and-it-changed-how-i-write-php-36m1</link>
      <guid>https://forem.com/vbcherepanov/i-studied-the-etcd-codebase-and-it-changed-how-i-write-php-36m1</guid>
      <description>&lt;p&gt;There's a common piece of advice: "Want to write better code? Read good code." Sounds obvious. Rarely practiced.&lt;/p&gt;

&lt;p&gt;The problem is that most open-source projects are mazes. You open a repo, see 200 directories, and close the tab. Kubernetes is two million lines. The Linux kernel — don't even think about it. Where do you start?&lt;/p&gt;

&lt;p&gt;My answer: &lt;strong&gt;etcd&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For those unfamiliar: etcd is a distributed key-value store written in Go. It's the backbone of Kubernetes — every piece of cluster state lives there. But I'm not interested in etcd as a product. I'm interested in it as &lt;strong&gt;an example of architecture you can actually read from start to finish&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what surprised me: the principles baked into etcd aren't about Go. They're about software design in general. I work with PHP and Symfony daily, and almost everything I found in etcd translated directly into my projects.&lt;/p&gt;

&lt;p&gt;Seven principles, concrete examples, no fluff.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. One Source of Truth for Your API
&lt;/h2&gt;

&lt;p&gt;In etcd, every API is defined in &lt;code&gt;.proto&lt;/code&gt; files. Open &lt;code&gt;rpc.proto&lt;/code&gt; and you see all operations: &lt;code&gt;Range&lt;/code&gt;, &lt;code&gt;Put&lt;/code&gt;, &lt;code&gt;DeleteRange&lt;/code&gt;, &lt;code&gt;Txn&lt;/code&gt;. Every field is typed. There's no room for "wait, do we accept a string or an integer here?"&lt;/p&gt;

&lt;p&gt;In PHP, instead of protobuf, we have &lt;strong&gt;strictly typed DTOs&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="cd"&gt;/** @var OrderItemDto[] */&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$promoCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One class — and everyone knows what the endpoint accepts. The frontend dev looks at the DTO, the backend dev writes logic against it, the OpenAPI schema generates automatically via NelmioApiDocBundle.&lt;/p&gt;

&lt;p&gt;Compare this with what I've seen (and written) on real projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&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;// What's the format of items? Is promoCode a thing? Who knows.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When your contract is "well, some array comes in," any change breaks something unexpected. When your contract is a DTO with types, PHPStan catches the problem before production does.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Each Service Does One Thing
&lt;/h2&gt;

&lt;p&gt;etcd has clearly separated gRPC services: &lt;code&gt;KV&lt;/code&gt; (read-write), &lt;code&gt;Watch&lt;/code&gt; (subscribe to changes), &lt;code&gt;Lease&lt;/code&gt; (key TTLs), &lt;code&gt;Auth&lt;/code&gt; (authorization). Each one is a separate interface. &lt;code&gt;Watch&lt;/code&gt; doesn't touch writes. &lt;code&gt;KV&lt;/code&gt; doesn't check tokens.&lt;/p&gt;

&lt;p&gt;In Symfony — same idea, different tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Route('/orders', methods: ['POST'])]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;OrderService&lt;/span&gt; &lt;span class="nv"&gt;$orderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&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;new&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$orderService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OrderService&lt;/code&gt; creates orders. It doesn't send emails — that's &lt;code&gt;NotificationService&lt;/code&gt; listening to an &lt;code&gt;OrderCreatedEvent&lt;/code&gt;. It doesn't process payments — that's &lt;code&gt;PaymentService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And then there's the alternative I see regularly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 40 lines of validation&lt;/span&gt;
        &lt;span class="c1"&gt;// 20 lines of authorization&lt;/span&gt;
        &lt;span class="c1"&gt;// 60 lines of business logic&lt;/span&gt;
        &lt;span class="c1"&gt;// 15 lines sending email&lt;/span&gt;
        &lt;span class="c1"&gt;// 10 lines of logging&lt;/span&gt;
        &lt;span class="c1"&gt;// Total: 150 lines, untestable&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;The 500-line god controller. We've all been there. etcd helped me finally articulate &lt;em&gt;why&lt;/em&gt; it's bad: not because "the pattern is wrong," but because &lt;strong&gt;you can't trace what the system is doing&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Middleware Composes Like Lego
&lt;/h2&gt;

&lt;p&gt;Every gRPC request in etcd passes through a chain of interceptors: logging → auth → metrics → handler → metrics → response. Each interceptor is small, single-purpose. The power comes from composition.&lt;/p&gt;

&lt;p&gt;In Symfony, this maps to Event Listeners and Messenger Middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MetricsMiddleware&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;MiddlewareInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PrometheusCollector&lt;/span&gt; &lt;span class="nv"&gt;$metrics&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Envelope&lt;/span&gt; &lt;span class="nv"&gt;$envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;StackInterface&lt;/span&gt; &lt;span class="nv"&gt;$stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Envelope&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&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="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stack&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$stack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'messages_processed_total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$envelope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'messages_processed_total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$envelope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;histogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'message_duration_seconds'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$envelope&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&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="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;One middleware, one job. Metrics here, logging there, retry somewhere else. Assemble the chain in &lt;code&gt;messenger.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The antipattern — when every handler has this manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CreateOrderCommand&lt;/span&gt; &lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Starting order creation...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ... actual logic ...&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Order created'&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;50 handlers, 50 copies of the same boilerplate. Forget one — no metrics. Change the log format — change it in 50 places.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Observability Is Architecture, Not an Afterthought
&lt;/h2&gt;

&lt;p&gt;In etcd, Prometheus is wired into the gRPC layer from day one. Not "added six months after launch." The code isn't considered done without metrics.&lt;/p&gt;

&lt;p&gt;In PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;PaymentResult&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment_charge_duration'&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="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payments_total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'provider'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'declined'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GatewayTimeoutException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payments_total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'provider'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paymentMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$timer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;observe&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every payment — in metrics. How many succeeded, how many timed out, which provider is slow. Not because someone asked for it, but because without it you're flying blind.&lt;/p&gt;

&lt;p&gt;I remember a project where production was down for 40 minutes and the only way to understand what was happening was &lt;code&gt;tail -f /var/log/symfony.log | grep ERROR&lt;/code&gt;. Never again.&lt;/p&gt;

&lt;p&gt;Package: &lt;code&gt;promphp/prometheus_client_php&lt;/code&gt;. Five minutes to install, fifteen to wire up Grafana.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Simple Outside, Rocket Science Inside
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clientv3&lt;/code&gt; in etcd is a masterclass in the facade pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Under the hood: node selection, reconnection on failure, retry with exponential backoff, protobuf serialization, Raft consensus, disk write, quorum confirmation.&lt;/p&gt;

&lt;p&gt;Same principle in PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calling code. Simple and clear.&lt;/span&gt;
&lt;span class="nv"&gt;$paymentService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;charge()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;PaymentResult&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findExistingPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$existing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idempotency&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;providerResolver&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$provider&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;maxAttempts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'exponential'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fiscalService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PaymentProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&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;The controller calling &lt;code&gt;charge()&lt;/code&gt; knows nothing about fiscal receipts, retries, or provider selection. And it shouldn't.&lt;/p&gt;

&lt;p&gt;A sign of a good service: you can explain what it does in one sentence — "charges the customer for an order" — while the implementation is 200 lines of careful logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. You Can Trace a Request With Your Finger
&lt;/h2&gt;

&lt;p&gt;In etcd, the request path reads linearly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gRPC handler → EtcdServer.Put() → Raft → apply → bbolt (disk)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No magic. No hidden calls. No "where does this even get triggered?"&lt;/p&gt;

&lt;p&gt;In Symfony — same thing, if you don't abuse the event system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
  → Controller (unwrap DTO)
    → Service (business logic)
      → Repository (database)
      → EventDispatcher (side effects)
  → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the controller — see which service is called. Open the service — see what it does. Open the repository — see the query.&lt;/p&gt;

&lt;p&gt;What kills traceability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@PostPersist&lt;/code&gt; on an entity that silently sends SMS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prePersist&lt;/code&gt; listeners modifying data before writes — and you spend 30 minutes figuring out who's touching the &lt;code&gt;updatedAt&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;Ten &lt;code&gt;EventSubscriber&lt;/code&gt;s on the same event with unclear execution order
Event-driven is great. But if a new developer can't explain "request comes in here, response goes out there" within 2 minutes — you have a problem.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. No Hidden Dependencies
&lt;/h2&gt;

&lt;p&gt;In etcd, all dependencies are passed explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewKVServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EtcdServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;KVServer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the constructor — see everything the class needs.&lt;/p&gt;

&lt;p&gt;In Symfony — constructor injection, same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;OrderRepository&lt;/span&gt; &lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;EventDispatcherInterface&lt;/span&gt; &lt;span class="nv"&gt;$events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four dependencies. All visible. Want to test? Swap in mocks. Want to understand the class? Look at the constructor.&lt;/p&gt;

&lt;p&gt;Antipatterns that still survive in the wild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Service locator: where did this come from?&lt;/span&gt;
&lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment.gateway'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Static calls: untestable&lt;/span&gt;
&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// new SomeService() inside another service: invisible coupling&lt;/span&gt;
&lt;span class="nv"&gt;$validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderValidator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Symfony's autowiring isn't magic in the bad sense. The container wires dependencies by type, but you still see them in the constructor. It's convenience, not hidden behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Checklist
&lt;/h2&gt;

&lt;p&gt;After studying etcd, I distilled a checklist I now apply to every new service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contract defined?&lt;/strong&gt; DTOs exist, types are set, OpenAPI generates from them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller thin?&lt;/strong&gt; 10 lines max, all logic in the service layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-cutting concerns extracted?&lt;/strong&gt; Logging, metrics, retry — through middleware, not copy-paste&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics present?&lt;/strong&gt; If not, the service isn't production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple API externally?&lt;/strong&gt; Calling code doesn't know about internal complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request path traceable?&lt;/strong&gt; A new developer finds the handler in 2 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies explicit?&lt;/strong&gt; Everything in the constructor, nothing from thin air
None of this is revolutionary. It's basic hygiene that's easy to forget under deadline pressure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;etcd just reminded me what a codebase looks like when that hygiene wasn't skipped. And that it's possible even in a large production system.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What open-source codebase changed how you write code? I'd love to build a reading list — drop yours in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>learning</category>
      <category>opensource</category>
      <category>php</category>
    </item>
  </channel>
</rss>
