<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Home on Vitalii Sukach</title><link>https://sukachvitally.github.io/</link><description>Recent content in Home on Vitalii Sukach</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sat, 17 Oct 2026 13:01:45 +0000</lastBuildDate><atom:link href="https://sukachvitally.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>RAG in Java with LangChain4j: Teaching a Local LLM About a Company That Doesn’t Exist</title><link>https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/</link><pubDate>Sat, 17 Oct 2026 13:01:45 +0000</pubDate><guid>https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/</guid><description>&lt;p>&lt;em>Chunks, vectors, a bit of geometry, and no magic. Everything runs on your laptop.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-01.png">&lt;/p>
&lt;p>Ask an LLM “When does the nightly settlement batch run at Harbourline Payments?” and one of two things happens.&lt;/p>
&lt;p>The polite model says it doesn’t know. The confident model says “Usually around midnight!” with the energy of a student who didn’t read the book but really wants a good mark.&lt;/p>
&lt;p>Both are fair, because Harbourline Payments doesn’t exist. I made it up. It’s a fictional company with a fictional engineering handbook: founders, services, deploy rules, on-call pay, and even a famous incident called “the Double Tap”. No model has seen this text during training.&lt;/p></description><content:encoded><![CDATA[<p><em>Chunks, vectors, a bit of geometry, and no magic. Everything runs on your laptop.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-01.png"></p>
<p>Ask an LLM “When does the nightly settlement batch run at Harbourline Payments?” and one of two things happens.</p>
<p>The polite model says it doesn’t know. The confident model says “Usually around midnight!” with the energy of a student who didn’t read the book but really wants a good mark.</p>
<p>Both are fair, because Harbourline Payments doesn’t exist. I made it up. It’s a fictional company with a fictional engineering handbook: founders, services, deploy rules, on-call pay, and even a famous incident called “the Double Tap”. No model has seen this text during training.</p>
<p>That’s exactly why it’s a good test. If the model answers “02:30 UK time” correctly, it didn’t remember it. It <strong>found</strong> it. That’s RAG.</p>
<p>In this article we’ll build a small RAG service in Java with Spring Boot, LangChain4j, PostgreSQL with pgvector, and Ollama. No cloud, no API keys, no bill at the end of the month.</p>
<p>The full code is on GitHub: <a href="https://github.com/SukachVitally/rag-demo">SukachVitally/rag-demo</a>.</p>
<h3 id="what-is-rag-in-oneexam">What is RAG, in one exam</h3>
<p>RAG stands for <strong>Retrieval-Augmented Generation</strong>. Long name, simple idea.</p>
<p>Think about two exams:</p>
<ul>
<li><strong>Closed-book exam.</strong> The student answers from memory. This is a normal LLM. It knows a lot, but only what it learned during training. It knows nothing about your company’s wiki, last week’s incident, or your internal rules. If it doesn’t know, it may still try to sound smart.</li>
<li><strong>Open-book exam.</strong> Before answering, the student finds the right pages in the book and reads them. This is RAG.</li>
</ul>
<p>So RAG doesn’t make the model smarter. It gives the model a cheat sheet for each question: “Here are 5 pieces of text that look relevant. Answer using only these.”</p>
<p>Why not just put the whole handbook into the prompt? For one small file you can. But real knowledge bases have thousands of documents. They don’t fit into the context window, and even when they do, you pay for every token on every question. RAG sends only the few pieces that matter.</p>
<h3 id="the-bigidea">The big idea</h3>
<p>The whole thing is two flows.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-02.png"></p>
<p><strong>Ingest (once per document):</strong></p>
<ol>
<li>Take the text and split it into small pieces called <strong>chunks</strong>.</li>
<li>Ask an <strong>embedding model</strong> to turn each chunk into a vector: a long list of numbers.</li>
<li>Save each chunk together with its vector in the database.</li>
</ol>
<p><strong>Ask (every question):</strong></p>
<ol>
<li>Ask the <strong>same</strong> embedding model to turn the question into a vector.</li>
<li>Find the chunks whose vectors are <strong>closest</strong> to the question vector.</li>
<li>Send the question and those chunks to a <strong>chat model</strong> and ask it to answer using only these chunks.</li>
</ol>
<p>That’s it. The rest of this article is details.</p>
<p>Notice that there are <strong>two different models</strong>:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-03.png"></p>
<p>The embedding model is small and fast. The chat model is the big one. Both run locally in Ollama.</p>
<h3 id="wait-what-is-thisvector">Wait, what is this vector?</h3>
<p>This is the most interesting part, so let’s slow down a little.</p>
<p>An embedding model reads a piece of text and returns a fixed-size array of numbers. nomic-embed-text returns <strong>768</strong> numbers for any text, whether it&rsquo;s one word or a full paragraph:</p>
<pre tabindex="0"><code>&#34;settlement batch runs at 02:30&#34;  →  [-0.064, 0.142, 0.009, ... 765 more ...]
</code></pre><p>The model is trained so that texts with <strong>similar meaning</strong> get vectors that point in a <strong>similar direction</strong>. “When does the nightly batch run?” and “settlement-service runs the nightly batch at 02:30” share almost no grammar, but their meaning is close, so their vectors are close too.</p>
<p>Now the fun part. It’s natural to think that each number means something: dimension 17 is “money”, dimension 302 is “time of day”, dimension 555 is “how angry the on-call engineer is”. It doesn’t work like that.</p>
<p><strong>No single dimension has a physical meaning.</strong> Nobody designed these axes. The model learned the layout by itself during training. Meaning is spread across all 768 numbers at once, a bit like a smell is spread across many different molecules. You can’t point at one number and say “this is the time part”. Only the vector as a whole means something, and only when you compare it with other vectors.</p>
<p>Here’s a 2D sketch of what that looks like. The real space has 768 dimensions, and I have only two axes on my screen:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-04.png"></p>
<p>The question vector points almost in the same direction as the “settlement batch runs at 02:30” chunk. The sourdough recipe chunk points somewhere else entirely. So for a computer, “similar meaning” becomes simple geometry: <strong>a small angle between two vectors</strong>.</p>
<p>The usual way to measure this is <strong>cosine similarity</strong>, the cosine of that angle:</p>
<ul>
<li>1: same direction, very similar</li>
<li>0: unrelated</li>
<li>-1: opposite direction</li>
</ul>
<p>LangChain4j turns it into a friendlier score from 0 to 1: (1 + cos θ) / 2. That&rsquo;s the score you&rsquo;ll see in the API response later.</p>
<p>One important rule: vectors from different embedding models are <strong>not compatible</strong>. Each model builds its own space with its own layout. If you change the embedding model, you have to re-embed all your documents. It’s like changing the map projection: the old coordinates don’t match the new map.</p>
<h3 id="the-stack">The stack</h3>
<ul>
<li><strong>Java 25</strong> and <strong>Spring Boot 4.1</strong>: the usual REST app.</li>
<li><strong>LangChain4j 1.22</strong>: the Java library that does the AI work.</li>
<li><strong>Ollama</strong>: runs both models locally.</li>
<li><strong>PostgreSQL 17 + pgvector</strong>: stores chunks and vectors, and searches by similarity.</li>
<li><strong>Testcontainers</strong>: for functional tests with a real database.</li>
</ul>
<p>Run the infrastructure with one command:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/docker-compose.yaml"><em>docker-compose.yaml</em></a></p>
<pre tabindex="0"><code>services:
  db:
    image: pgvector/pgvector:pg17
    environment:
      POSTGRES_DB: rag
      POSTGRES_USER: rag
      POSTGRES_PASSWORD: rag
    ports: [&#34;5432:5432&#34;]
  ollama:
    image: ollama/ollama
    ports: [&#34;11434:11434&#34;]
    volumes:
      - ollama-models:/root/.ollama   # keeps downloaded models between restarts
  ollama-init:
    image: ollama/ollama
    depends_on: [ollama]
    environment:
      OLLAMA_HOST: ollama:11434
    entrypoint: [&#34;sh&#34;, &#34;-c&#34;, &#34;sleep 5 &amp;&amp; ollama pull nomic-embed-text &amp;&amp; ollama pull llama3.1:8b&#34;]
volumes:
  ollama-models:
</code></pre><p>The ollama-init container is a one-off job that downloads the models. The first time it pulls a few GB, so it&rsquo;s a good moment to make some tea.</p>
<h3 id="langchain4j-ai-lego-forjava">LangChain4j: AI LEGO for Java</h3>
<p>LangChain4j is to AI roughly what Spring Data is to databases. It gives you small interfaces with many implementations, so your code doesn’t depend on a specific vendor.</p>
<p>The demo uses four building blocks, plus one helper that glues three of them together:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-05.png"></p>
<ul>
<li><strong>DocumentSplitter</strong> cuts text into chunks.</li>
<li><strong>EmbeddingModel</strong> turns text into a vector. Here it&rsquo;s OllamaEmbeddingModel.</li>
<li><strong><code>EmbeddingStore&lt;TextSegment&gt;</code></strong> saves vectors and finds the nearest ones. Here it&rsquo;s PgVectorEmbeddingStore.</li>
<li><strong>ChatModel</strong> sends a prompt and gets an answer. Here it&rsquo;s OllamaChatModel.</li>
<li><strong>EmbeddingStoreIngestor</strong> runs split → embed → store in one call.</li>
</ul>
<p>The nice part is the bottom of that picture. Want OpenAI instead of Ollama? Change one bean. Want Qdrant instead of Postgres? Change one bean. The services don’t care.</p>
<h3 id="dependencies">Dependencies</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/build.gradle"><em>build.gradle</em></a></p>
<pre tabindex="0"><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-jdbc&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-webmvc&#39;
    implementation &#39;dev.langchain4j:langchain4j:1.22.0&#39;
    implementation &#39;dev.langchain4j:langchain4j-ollama:1.22.0&#39;
    implementation &#39;dev.langchain4j:langchain4j-pgvector:1.22.0-beta32&#39;
    runtimeOnly &#39;org.postgresql:postgresql&#39;
}
</code></pre><p>Three LangChain4j modules: the core, the Ollama integration and the pgvector store. That’s all.</p>
<h3 id="wiring-thebeans">Wiring the beans</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/src/main/java/org/example/ragdemo/config/LangChain4jConfig.java"><em>LangChain4jConfig.java</em></a></p>
<pre tabindex="0"><code>@Configuration
public class LangChain4jConfig {
    public static final String CHUNK_TABLE = &#34;chunk_embedding&#34;;
    @Bean
    EmbeddingModel embeddingModel(OllamaClientProperty property) {
        return OllamaEmbeddingModel.builder()
                .baseUrl(property.baseUrl())
                .modelName(property.embedModel())
                .build();
    }
    @Bean
    ChatModel chatModel(OllamaClientProperty property) {
        return OllamaChatModel.builder()
                .baseUrl(property.baseUrl())
                .modelName(property.chatModel())
                .build();
    }
    @Bean
    DocumentSplitter documentSplitter(ChunkProperty property) {
        return DocumentSplitters.recursive(property.size(), property.overlap());
    }
    @Bean
    EmbeddingStore&lt;TextSegment&gt; embeddingStore(DataSource dataSource) {
        return PgVectorEmbeddingStore.datasourceBuilder()
                .datasource(dataSource)
                .table(CHUNK_TABLE)
                .createTable(false)
                .skipCreateVectorExtension(true)
                .metadataStorageConfig(DefaultMetadataStorageConfig.builder()
                        .storageMode(MetadataStorageMode.COMBINED_JSONB)
                        .columnDefinitions(List.of(&#34;metadata JSONB NULL&#34;))
                        .build())
                .build();
    }
}
</code></pre><p>The embedding store reuses Spring’s DataSource, so there&rsquo;s one connection pool and one config. I also told it <strong>not</strong> to create the table (createTable(false)) because I prefer to own the schema myself:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/src/main/resources/schema.sql"><em>schema.sql</em></a></p>
<pre tabindex="0"><code>CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS chunk_embedding (
    embedding_id UUID PRIMARY KEY,
    embedding    vector(768) NOT NULL,
    text         TEXT NULL,
    metadata     JSONB NULL
);
CREATE INDEX IF NOT EXISTS chunk_embedding_hnsw
    ON chunk_embedding USING hnsw (embedding vector_cosine_ops);
</code></pre><p>Two things to notice:</p>
<ul>
<li>vector(768) is the pgvector column type. 768 must match the embedding model&rsquo;s output. If you switch to a model with 1024 dimensions, this column has to change too.</li>
<li>The <strong>HNSW index</strong> with vector_cosine_ops makes nearest-neighbour search fast. Without it, Postgres compares the question with every row. That&rsquo;s fine for 9 chunks, but not for 9 million. HNSW is an approximate search: it trades a tiny bit of accuracy for a lot of speed.</li>
</ul>
<p>Everything is configurable in application.yaml:</p>
<pre tabindex="0"><code>app:
  olama-client:
    base-url: http://localhost:11434
    embed-model: nomic-embed-text
    chat-model: llama3.1:8b
  chunk:
    size: 1000
    overlap: 150
</code></pre><h3 id="part-1-ingest-or-feeding-thedatabase">Part 1: Ingest, or feeding the database</h3>
<p>Here’s what happens to a document on the way in:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/rag-in-java-with-langchain4j-teaching-a-local-llm-about-a-company-that-doesnt-exist/image-06.png"></p>
<h3 id="why-chunks">Why chunks?</h3>
<p>Why not embed the whole document as one vector? Because one vector for a whole handbook would be an average of everything: founders, deploy rules, on-call pay, the sourdough recipe someone accidentally uploaded. It would be “kind of about everything”, and that means it’s close to nothing.</p>
<p>Small chunks keep the meaning sharp. One chunk is about the settlement batch, another is about deploy windows. Now the search can find the right one.</p>
<p>The demo uses DocumentSplitters.recursive(1000, 150):</p>
<ul>
<li><strong>1000</strong>: the maximum chunk size in characters.</li>
<li><strong>150</strong>: overlap. Each chunk repeats about 150 characters from the end of the previous one. So if an important sentence lands on a border, it’s still complete in at least one chunk.</li>
<li><strong>Recursive</strong> means it tries to cut nicely. First by paragraphs. If a paragraph is too big, then by lines, then by sentences, then by words. It cuts the text like a polite butcher, not with a chainsaw.</li>
</ul>
<h3 id="the-ingestservice">The ingest service</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/src/main/java/org/example/ragdemo/service/IngestService.java"><em>IngestService.java</em></a></p>
<pre tabindex="0"><code>@Service
@RequiredArgsConstructor
public class IngestService {
    public static final String SOURCE = &#34;source&#34;;
    private final DocumentSplitter splitter;
    private final EmbeddingModel embeddingModel;
    private final EmbeddingStore&lt;TextSegment&gt; store;
    
    public int ingest(String source, String text) {
        if (text.isBlank()) return 0;
        var chunks = new AtomicInteger();
        EmbeddingStoreIngestor.builder()
                .documentSplitter(splitter)
                .textSegmentTransformer(segment -&gt; { chunks.incrementAndGet(); return segment; })
                .embeddingModel(embeddingModel)
                .embeddingStore(store)
                .build()
                .ingest(Document.from(text, Metadata.from(SOURCE, source)));
        return chunks.get();
    }
}
</code></pre><p>EmbeddingStoreIngestor does all three steps for us: split, embed, store. The textSegmentTransformer is a hook that runs for every chunk. Here I only use it to count chunks for the API response, but it&rsquo;s also the right place to clean text or add extra metadata.</p>
<p>The source metadata (the file name) travels with every chunk into the metadata JSONB column. Later it lets the answer say <em>where</em> the information came from.</p>
<p>The controller is a plain multipart upload:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/src/main/java/org/example/ragdemo/controller/RagController.java"><em>RagController.java</em></a></p>
<pre tabindex="0"><code>@PostMapping(value = &#34;/ingest&#34;, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Map&lt;String, Object&gt; upload(@RequestParam MultipartFile file) throws IOException {
    String text = new String(file.getBytes(), StandardCharsets.UTF_8);
    return Map.of(&#34;chunks&#34;, ingest.ingest(file.getOriginalFilename(), text));
}
</code></pre><p>Let’s feed it the Harbourline handbook:</p>
<pre tabindex="0"><code>curl -F file=@docs/notes.md http://localhost:8080/api/ingest
</code></pre><pre tabindex="0"><code>{ &#34;chunks&#34;: 9 }
</code></pre><p>Nine chunks, nine vectors, nine rows. The database now knows about a company that doesn’t exist.</p>
<h3 id="part-2-ask-or-the-open-book-exam">Part 2: Ask, or the open-book exam</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/rag-demo/blob/main/src/main/java/org/example/ragdemo/service/RagService.java"><em>RagService.java</em></a></p>
<pre tabindex="0"><code>@Service
@RequiredArgsConstructor
public class RagService {
    private final EmbeddingModel embeddingModel;
    private final EmbeddingStore&lt;TextSegment&gt; store;
    private final ChatModel chatModel;
    private static final String SYSTEM = &#34;&#34;&#34;
        Answer using ONLY the provided context. If the answer is not in the
        context, say you don&#39;t know. Cite sources as [1], [2]...&#34;&#34;&#34;;
    
public record Answer(String answer, List&lt;Hit&gt; sources) {}
    public Answer ask(String question) {
        var q = embeddingModel.embed(question).content();
        var hits = store.search(EmbeddingSearchRequest.builder().queryEmbedding(q).maxResults(5).build())
                .matches().stream()
                .map(m -&gt; new Hit(m.embedded().metadata().getString(IngestService.SOURCE), m.embedded().text(), m.score()))
                .toList();
        var ctx = new StringBuilder();
        for (int i = 0; i &lt; hits.size(); i++)
            ctx.append(&#34;[&#34;).append(i + 1).append(&#34;] (&#34;).append(hits.get(i).source()).append(&#34;)\n&#34;)
                    .append(hits.get(i).content()).append(&#34;\n\n&#34;);
        String prompt = &#34;Context:\n&#34; + ctx + &#34;\nQuestion: &#34; + question;
        return new Answer(chatModel.chat(SystemMessage.from(SYSTEM), UserMessage.from(prompt)).aiMessage().text(), hits);
    }
}
</code></pre><p>This is the heart of the project, and it follows the plan exactly:</p>
<ol>
<li><strong>Embed the question</strong> with the same model we used for the chunks. Same model means same space, so the distances make sense.</li>
<li><strong>Search</strong> the store for the 5 nearest vectors. Under the hood, pgvector uses the HNSW index and cosine distance.</li>
<li><strong>Build the context</strong>: number each chunk and add its source.</li>
<li><strong>Ask the chat model</strong> with a strict system message: use only the context, say “I don’t know” if the answer isn’t there, and cite sources.</li>
</ol>
<p>This is roughly what llama actually receives:</p>
<pre tabindex="0"><code>SYSTEM:
Answer using ONLY the provided context. If the answer is not in the
context, say you don&#39;t know. Cite sources as [1], [2]...
</code></pre><pre tabindex="0"><code>USER:
Context:
[1] (notes.md)
... settlement-service — runs the nightly settlement batch at 02:30 UK time ...
</code></pre><pre tabindex="0"><code>[2] (notes.md)
...
</code></pre><pre tabindex="0"><code>Question: When does the nightly settlement batch run?
</code></pre><p>The chat model never sees the database, the vectors or the other chunks. It sees a small, focused cheat sheet and a question. Its job is now easy: read and summarise.</p>
<p>Let’s try it:</p>
<pre tabindex="0"><code>curl -H &#39;Content-Type: application/json&#39; \
     -d &#39;{&#34;question&#34;: &#34;When does the nightly settlement batch run?&#34;}&#39; \
     http://localhost:8080/api/ask
</code></pre><pre tabindex="0"><code>{
  &#34;answer&#34;: &#34;According to the context in [3] (notes.md), the nightly settlement batch runs at 02:30 UK time.&#34;,
  &#34;sources&#34;: [
    { &#34;source&#34;: &#34;notes.md&#34;, &#34;content&#34;: &#34;...&#34;, &#34;score&#34;: 0.79 }
  ]
}
</code></pre><p>02:30 UK time. Correct, and with a source. Open-book exam passed. 🎓</p>
<p>The sources list matters as much as the answer. You can show it to users, so they can check where the answer came from instead of trusting the model blindly. Trust, but verify, especially with an 8B model.</p>
<h3 id="things-to-keep-inmind">Things to keep in mind</h3>
<p>The demo is small on purpose. Before you take it to production, think about these:</p>
<ul>
<li><strong>Chunk size is a trade-off.</strong> Too small, and a chunk loses context (“it runs at 02:30”, but what is “it”?). Too big, and the meaning gets blurry again. 1000/150 is a reasonable start, not a law.</li>
<li><strong>The top 5 are always returned,</strong> even when nothing is relevant. Ask about pizza, and you’ll still get the 5 “least irrelevant” chunks. The strict system prompt helps the model say “I don’t know”. In a real system I’d also add a minimum score (EmbeddingSearchRequest supports minScore).</li>
<li><strong>Changing the embedding model means re-ingesting everything.</strong> Different model, different space, different vector(N) column.</li>
<li><strong>Re-uploading the same file creates duplicates.</strong> The demo only appends. A real system needs to update or delete chunks by source.</li>
<li><strong>Garbage in, garbage out.</strong> RAG is only as good as the documents you give it. If the wiki is wrong, the answer will be wrong, but now with a citation.</li>
</ul>
<h3 id="summary">Summary</h3>
<p>RAG sounds like a fancy AI term, but in code it’s a short pipeline:</p>
<ul>
<li><strong>Ingest:</strong> split text into chunks → embed each chunk into a vector → store the text and vector in pgvector.</li>
<li><strong>Ask:</strong> embed the question → find the closest chunk vectors → give the question and those chunks to the chat model → answer with sources.</li>
</ul>
<p>The vectors are the only “magic” part. They are 768 numbers with no human meaning in any single dimension, but their direction captures meaning. Similar text means a small angle, and a small angle is something a database can search.</p>
<p>LangChain4j turns each step into one small interface, so the whole service is a handful of Spring beans. You can swap Ollama for OpenAI, or pgvector for Qdrant, without touching the business code.</p>
<p>And now a local 8B model knows when Harbourline’s settlement batch runs. Harbourline still doesn’t exist, but at least its batch is on time. ⏰</p>
<p>The full code is on GitHub: <a href="https://github.com/SukachVitally/rag-demo">SukachVitally/rag-demo</a>. Clone it, run docker compose up -d, upload your own notes and ask them questions.</p>
]]></content:encoded></item><item><title>Building an MCP Server in Java with Spring AI: Let Claude Code Use Your Database Without Seeing a Password</title><link>https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/</link><pubDate>Fri, 25 Sep 2026 14:46:28 +0000</pubDate><guid>https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/</guid><description>&lt;p>&lt;em>Four annotated methods, one security config, and no JSON-RPC code of your own.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/image-01.png">&lt;/p>
&lt;p>In my &lt;a href="https://medium.com/@sukach.v.v/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless-759de3745b79">previous article&lt;/a> I built an MCP server from scratch in Python. I wrote the JSON-RPC 2.0 message handling, declared tool schemas by hand and wired up the STDIO transport myself. It’s a good way to learn how the protocol works on the wire.&lt;/p>
&lt;p>Python isn’t the only option, though. Most of the systems I work on are Java and Spring, and that’s where the data lives. So this time I built the same kind of server in Java.&lt;/p></description><content:encoded><![CDATA[<p><em>Four annotated methods, one security config, and no JSON-RPC code of your own.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/image-01.png"></p>
<p>In my <a href="https://medium.com/@sukach.v.v/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless-759de3745b79">previous article</a> I built an MCP server from scratch in Python. I wrote the JSON-RPC 2.0 message handling, declared tool schemas by hand and wired up the STDIO transport myself. It’s a good way to learn how the protocol works on the wire.</p>
<p>Python isn’t the only option, though. Most of the systems I work on are Java and Spring, and that’s where the data lives. So this time I built the same kind of server in Java.</p>
<p>It turns out you don’t need to implement JSON-RPC yourself. Spring AI has a Boot starter for MCP servers. It handles the protocol, the transport, schema generation and tool registration. You write ordinary Spring beans and add a few annotations.</p>
<p>The full code is on GitHub: <a href="https://github.com/SukachVitally/mcp-demo">SukachVitally/mcp-demo</a>.</p>
<h3 id="why-bother-the-server-is-theboundary">Why bother: the server is the boundary</h3>
<p>The business logic here is deliberately boring. It’s a task list: you create tasks, move them through statuses, and finish or cancel them. Swap in your own domain; the MCP part stays the same.</p>
<p>The point is how Claude Code reaches the data:</p>
<pre tabindex="0"><code>Claude Code ──HTTP + API key──▶ MCP server (Spring) ──JDBC──▶ PostgreSQL
</code></pre><ul>
<li><strong>Claude never sees database credentials.</strong> The server holds the connection. Claude holds only an API key that works for the MCP endpoint and nothing else.</li>
<li><strong>Claude can only do what you expose.</strong> There’s no SQL console. If you don’t write a delete_task tool, tasks can&rsquo;t be deleted, however the prompt is phrased.</li>
<li><strong>Your validation and transactions still apply.</strong> Every call goes through the same service layer as the rest of your application.</li>
</ul>
<p>Giving an agent a database URL and a password is quick, but you can’t take it back. An MCP server gives the agent a small, typed and auditable API instead.</p>
<h3 id="what-werebuilding">What we’re building</h3>
<p>A PostgreSQL table of tasks. Each task has a name and one of four statuses: NEW, IN_PROGRESS, DONE or CANCELLED. The server exposes four tools:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/image-02.png"></p>
<p>Stack: Java 25, Spring Boot 4, Spring AI 2.0 MCP server, PostgreSQL 18 with Liquibase, and Spring Security.</p>
<h3 id="step-1-dependencies">Step 1: Dependencies</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/build.gradle"><em>build.gradle</em></a></p>
<pre tabindex="0"><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-liquibase&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-webmvc&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;
    implementation platform(&#39;org.springframework.ai:spring-ai-bom:2.0.1&#39;)
    implementation &#39;org.springframework.ai:spring-ai-starter-mcp-server-webmvc&#39;
    implementation &#39;org.springaicommunity:mcp-server-security-spring-boot:0.1.14&#39;
    runtimeOnly &#39;org.postgresql:postgresql&#39;
}
</code></pre><p>Two lines do the MCP work:</p>
<ul>
<li>spring-ai-starter-mcp-server-webmvc is the MCP server over HTTP on Spring MVC. There&rsquo;s a -webflux variant for reactive apps, and a plain spring-ai-starter-mcp-server for stdio.</li>
<li>mcp-server-security-spring-boot from the <a href="https://github.com/spring-ai-community/mcp-security">spring-ai-community/mcp-security</a> project adds API-key and OAuth2 support on top of Spring Security. We&rsquo;ll come back to it.</li>
</ul>
<h3 id="step-2-configure-theserver">Step 2: Configure the server</h3>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/main/resources/application.yaml"><em>application.yaml</em></a></p>
<pre tabindex="0"><code>spring:
  ai:
    mcp:
      server:
        protocol: STATELESS
        type: sync
        name: &#34;Task MCP demo&#34;
        instructions: &gt;-
          Manages a simple task list. Create tasks with add_task, look them up with get_task or
          get_task_list, and move them through the statuses NEW, IN_PROGRESS, DONE and CANCELLED
          with update_task_status.
        streamable-http:
          mcp-endpoint: /mcp
</code></pre><p>What these settings mean:</p>
<ul>
<li><strong>protocol: STATELESS</strong>: plain HTTP request and response, with no session kept between calls. Our tools never need to push anything back to the client, so we don&rsquo;t need sessions, and the server scales horizontally like any other REST service. Use STREAMABLE if you need server-to-client messages. The older SSE option is deprecated since Spring AI 2.0.</li>
<li><strong>type: sync</strong>: tools are ordinary blocking methods. With async, they return Reactor Mono/Flux types instead.</li>
<li><strong>instructions</strong>: sent to the client when it connects. It&rsquo;s the server&rsquo;s short explanation to the model of how the tools fit together. Treat it as part of the prompt.</li>
<li><strong>mcp-endpoint: /mcp</strong>: the URL that Claude Code will call.</li>
</ul>
<h3 id="step-3-business-logic-keep-itboring">Step 3: Business logic (keep it boring)</h3>
<p>Nothing here knows about MCP. It’s a normal JPA entity, repository and service:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/main/java/org/example/mcpdemo/service/TaskService.java"><em>TaskService.java</em></a></p>
<pre tabindex="0"><code>@Service
@Validated
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TaskService {
    public static final int NAME_MAX_LEN
GTH = 255;
    private final TaskRepository repository;
    private final TaskMapper mapper;
    @Transactional
    public TaskDto create(@NotBlank @Size(max = NAME_MAX_LENGTH) String name) {
        var entity = new TaskEntity();
        entity.setName(name);
        entity.setStatus(TaskStatus.NEW);
        return mapper.toDto(repository.save(entity));
    }
    @Transactional
    public TaskDto updateStatus(Long id, @NotNull TaskStatus status) {
        var entity = findEntity(id);
        entity.setStatus(status);
        return mapper.toDto(entity);
    }
    // get(id), getList(), findEntity(id) throws TaskNotFoundException ...
}
</code></pre><p>The service returns a small record, not the entity:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/main/java/org/example/mcpdemo/dto/TaskDto.java"><em>TaskDto.java</em></a></p>
<pre tabindex="0"><code>public record TaskDto(Long id, String name, TaskStatus status) {}
</code></pre><p>That matters more than it looks. Whatever a tool returns is serialized and handed to the model, so the DTO decides exactly which fields leave your system.</p>
<h3 id="step-4-taskmcptool-the-actual-mcpserver">Step 4: TaskMcpTool, the actual MCP server</h3>
<p>This is the whole MCP layer:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/main/java/org/example/mcpdemo/mcp/TaskMcpTool.java"><em>TaskMcpTool.java</em></a></p>
<pre tabindex="0"><code>@Component
@RequiredArgsConstructor
public class TaskMcpTool {
    private final TaskService service;
    @McpTool(name = &#34;add_task&#34;, description = &#34;Create a new task with status NEW and return it&#34;)
    public TaskDto add(@McpToolParam(description = &#34;Task name, 1-255 characters&#34;) String name) {
        return service.create(name);
    }
    @McpTool(name = &#34;get_task&#34;, description = &#34;Get a task by its id. Fails if the task does not exist&#34;)
    public TaskDto get(@McpToolParam(description = &#34;Task id&#34;) Long id) {
        return service.get(id);
    }
    @McpTool(name = &#34;update_task_status&#34;,
            description = &#34;Change the status of a task and return the updated task. Fails if the task does not exist&#34;)
    public TaskDto updateStatus(@McpToolParam(description = &#34;Task id&#34;) Long id,
                                @McpToolParam(description = &#34;New task status&#34;) TaskStatus status) {
        return service.updateStatus(id, status);
    }
    @McpTool(name = &#34;get_task_list&#34;, description = &#34;List all tasks&#34;)
    public List&lt;TaskDto&gt; getList() {
        return service.getList();
    }
}
</code></pre><p>Only a thin adapter: each method delegates to the service. Most of the work happens in the annotations.</p>
<h3 id="how-the-tools-get-registered">How the tools get registered</h3>
<p>The class is a plain @Component. When the application starts, Spring AI&rsquo;s annotation scanner finds every bean method annotated with @McpTool and registers it as an MCP tool. You don&rsquo;t need a registry, a ToolCallback list or any manual wiring. To add a tool, write another annotated method.</p>
<h3 id="mcptool-the-name-and-description-the-modelsees">@McpTool: the name and description the model sees</h3>
<ul>
<li><strong>name</strong> is the tool id the model calls. I use snake_case verbs (add_task, get_task_list) because that&rsquo;s the convention across MCP servers, and models pick such names up reliably. Without a name, the Java method name is used, and add on its own tells the model very little.</li>
<li><strong>description</strong> is the most important string in the class. It&rsquo;s the only thing the model reads when deciding which tool to call and what to expect back. Say what the tool does, what it returns and when it fails (&ldquo;Fails if the task does not exist&rdquo;). A vague description gives you a model that guesses.</li>
</ul>
<h3 id="mcptoolparam-the-schema-is-generated-foryou">@McpToolParam: the schema is generated for you</h3>
<p>Each parameter gets a description, and Spring AI builds a JSON Schema from the Java type. Here’s what the server actually returns for update_task_status when Claude Code asks for tools/list:</p>
<pre tabindex="0"><code>{
  &#34;name&#34;: &#34;update_task_status&#34;,
  &#34;description&#34;: &#34;Change the status of a task and return the updated task. Fails if the task does not exist&#34;,
  &#34;inputSchema&#34;: {
    &#34;type&#34;: &#34;object&#34;,
    &#34;properties&#34;: {
      &#34;id&#34;:     { &#34;type&#34;: &#34;integer&#34;, &#34;format&#34;: &#34;int64&#34;, &#34;description&#34;: &#34;Task id&#34; },
      &#34;status&#34;: { &#34;type&#34;: &#34;string&#34;,
                  &#34;enum&#34;: [&#34;NEW&#34;, &#34;IN_PROGRESS&#34;, &#34;DONE&#34;, &#34;CANCELLED&#34;],
                  &#34;description&#34;: &#34;New task status&#34; }
    },
    &#34;required&#34;: [&#34;id&#34;, &#34;status&#34;]
  }
}
</code></pre><p>Things to notice:</p>
<ul>
<li><strong>Long became</strong> <strong>integer with</strong> <strong>int64 format.</strong> You don&rsquo;t write any schema by hand.</li>
<li><strong>Parameters are required by default</strong> (required = true). Set @McpToolParam(required = false) for optional ones.</li>
<li><strong>The</strong> <strong>TaskStatus enum became a JSON</strong> <strong>enum.</strong> The model sees the four allowed values up front, so it can&rsquo;t invent a status like DELETED. If it tries, the call is rejected by schema validation before your method runs:</li>
</ul>
<pre tabindex="0"><code>Tool (update_task_status) input validation failed: ... /status: does not 
have a value in the enumeration [&#34;NEW&#34;, &#34;IN_PROGRESS&#34;, &#34;DONE&#34;, &#34;CANCELLED&#34;]
</code></pre><p>Use enums for anything with a fixed set of values. You get validation and documentation in one place.</p>
<h3 id="return-values-anderrors">Return values and errors</h3>
<p>You return a normal Java object, and Spring AI serializes it to JSON as the tool result. <code>TaskDto</code> and <code>List&lt;TaskDto&gt;</code> just work.</p>
<p>Exceptions work too. When TaskService throws TaskNotFoundException, the client doesn&rsquo;t get a stack trace or a broken JSON-RPC response. It gets a tool result with isError: true and the exception message as the text:</p>
<pre tabindex="0"><code>{ 
  &#34;content&#34;: [{ &#34;type&#34;: &#34;text&#34;, &#34;text&#34;: &#34;Task with id -1 is not found&#34; }], 
  &#34;isError&#34;: true 
}
</code></pre><p>That’s the behaviour you want: the model reads the message, understands what went wrong and can recover, for example by calling get_task_list to find the right id. Write exception messages for the model to read, not just for your logs.</p>
<p>The same applies to Bean Validation. A blank name fails @NotBlank in the service and comes back to the model as an error result, not a 500.</p>
<h3 id="a-tip-add-behaviour-hints">A tip: add behaviour hints</h3>
<p>If you look at the full tools/list response, every tool reports &ldquo;readOnlyHint&rdquo;: false, &ldquo;destructiveHint&rdquo;: true, including get_task_list. Those are the defaults. Clients such as Claude Code can use these hints to decide how cautious to be, so it&rsquo;s worth being honest about read-only tools:</p>
<pre tabindex="0"><code>@McpTool(name = &#34;get_task_list&#34;, description = &#34;List all tasks&#34;,
        annotations = @McpTool.McpAnnotations(readOnlyHint = true, 
                                              destructiveHint = false))
</code></pre><p>These are hints, not enforcement. What actually protects your data is the fact that the tool only reads.</p>
<h3 id="step-5-securityconfig-briefly">Step 5: SecurityConfig, briefly</h3>
<p>By default, an HTTP MCP endpoint is an unauthenticated JSON-RPC endpoint. Anyone who can reach /mcp can call your tools. The mcp-security library closes it with a few lines:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/main/java/org/example/mcpdemo/config/SecurityConfig.java"><em>SecurityConfig.java</em></a></p>
<pre tabindex="0"><code>@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http,
                                            ApiKeyEntityRepository&lt;ApiKeyEntityImpl&gt; apiKeyRepository) {
        return http
                .authorizeHttpRequests(auth -&gt; auth.anyRequest().authenticated())
                .with(mcpServerApiKey(), apiKey -&gt; apiKey.apiKeyRepository(apiKeyRepository))
                .build();
    }
    @Bean
    ApiKeyEntityRepository&lt;ApiKeyEntityImpl&gt; apiKeyRepository(@Value(&#34;${app.mcp.api-key.secret}&#34;) String secret) {
        var key = ApiKeyEntityImpl.builder()
                .name(&#34;demo key&#34;)
                .id(&#34;demo&#34;)
                .secret(secret)
                .build();
        return new InMemoryApiKeyEntityRepository&lt;&gt;(List.of(key));
    }
}
</code></pre><ul>
<li>mcpServerApiKey() plugs API-key authentication into the normal Spring Security filter chain.</li>
<li>The client sends an X-API-key header in the form <code>&lt;id&gt;.&lt;secret&gt;</code>, so <code>demo.&lt;your secret&gt;</code> here. Requests without it get 401.</li>
</ul>
<h3 id="step-6-run-it-and-connect-claudecode">Step 6: Run it and connect Claude Code</h3>
<p>Start PostgreSQL and the app:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/docker-compose.yaml"><em>docker-compose.yaml</em></a><em>,</em><a href="https://github.com/SukachVitally/mcp-demo/blob/main/.env.tpl"><em>.env.tpl</em></a><em>,</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/README.md"><em>README</em></a></p>
<pre tabindex="0"><code>cp .env.tpl .env          # sets MCP_API_KEY_SECRET
docker compose up -d
./gradlew bootRun
</code></pre><p>Register the server with Claude Code:</p>
<pre tabindex="0"><code>claude mcp add --transport http todos http://localhost:8080/mcp \
  --header &#34;X-API-key: demo.mcp-demo-test-secret&#34;
</code></pre><p>Then ask in plain language:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/building-an-mcp-server-in-java-with-spring-ai-let-claude-code-use-your-database-without-seeing-a/image-03.png"></p>
<p>“Add todo with name test” becomes an add_task call. &ldquo;Update task with id 1 to DONE&rdquo; becomes update_task_status with a valid enum value. Claude never saw a JDBC URL, a username or a password.</p>
<p>To inspect the server without Claude, use the MCP Inspector (npx @modelcontextprotocol/inspector). Choose the Streamable HTTP transport and add the same header.</p>
<h3 id="testing">Testing</h3>
<p>Because the server speaks standard MCP, the tests can use the official Java MCP client against a real server started by @SpringBootTest, with PostgreSQL in Testcontainers:</p>
<p><em>Source:</em> <a href="https://github.com/SukachVitally/mcp-demo/blob/main/src/test/java/org/example/mcpdemo/mcp/TaskMcpToolTest.java"><em>TaskMcpToolTest.java</em></a></p>
<pre tabindex="0"><code>var transport = HttpClientStreamableHttpTransport
        .builder(&#34;http://localhost:&#34; + port)
        .httpRequestCustomizer((builder, method, endpoint, body, context) -&gt;
                builder.header(&#34;X-API-key&#34;, &#34;demo.&#34; + API_KEY_SECRET))
        .build();
client = McpClient.sync(transport).build();
client.initialize();
var result = client.callTool(McpSchema.CallToolRequest.builder(&#34;get_task&#34;)
        .arguments(Map.of(&#34;id&#34;, -1L)).build());
assertThat(result.isError()).isTrue();
</code></pre><p>This covers the whole chain in one test: authentication, schema, serialization, error mapping and the database.</p>
<h3 id="takeaways">Takeaways</h3>
<ul>
<li><strong>You don’t write JSON-RPC in Java.</strong> spring-ai-starter-mcp-server-webmvc handles the protocol, the transport and tool discovery.</li>
<li><strong>An MCP tool is an annotated Spring bean method.</strong> @McpTool names and describes it, and @McpToolParam describes its inputs. The JSON Schema is generated from your Java types.</li>
<li><strong>Descriptions and error messages are prompts.</strong> The model reads them, so write them for the model.</li>
<li><strong>Use enums and DTOs</strong> to control what the model can send in and what it gets back.</li>
<li><strong>Secure the endpoint.</strong> API keys via mcp-security take ten lines, and OAuth2 is available when you need it.</li>
<li><strong>The MCP server is the security boundary.</strong> Claude Code works with your data through the operations you choose, never with your credentials.</li>
</ul>
<p>The Python version showed how MCP works on the wire. The Spring version shows how little code you need once a framework handles it. Take the repo, replace tasks with your own domain, and give your agent an API instead of a password.</p>
<p><strong>Links</strong></p>
<ul>
<li>Demo code: <a href="https://github.com/SukachVitally/mcp-demo">github.com/SukachVitally/mcp-demo</a></li>
<li>Spring AI MCP Server Boot Starter: <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html">docs.spring.io</a></li>
<li>MCP security for Spring: <a href="https://github.com/spring-ai-community/mcp-security/tree/main">github.com/spring-ai-community/mcp-security</a></li>
<li>Previous article, MCP in Python from scratch: <a href="https://medium.com/@sukach.v.v/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless-759de3745b79">MCP: The Protocol That Finally Taught AI to Stop Being Useless</a></li>
</ul>
]]></content:encoded></item><item><title>The Java Memory Model, Explained Without the Headache</title><link>https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/</link><pubDate>Thu, 03 Sep 2026 16:18:18 +0000</pubDate><guid>https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/</guid><description>&lt;h4 id="what-actually-happens-to-your-data-between-two-threadsand-the-small-set-of-tools-that-let-you-controlit">What actually happens to your data between two threads — and the small set of tools that let you control it&lt;/h4>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/image-01.png">&lt;/p>
&lt;p>Ask ten Java developers what “the Java Memory Model” means and you’ll get two different answers.&lt;/p>
&lt;p>Half of them will draw a box called &lt;em>Heap&lt;/em> and another called &lt;em>Stack&lt;/em>. The other half will start talking about volatile and happens-before.&lt;/p>
&lt;p>Both groups are talking about something real, but only the second group is talking about the actual &lt;strong>Java Memory Model (JMM)&lt;/strong>. The first group is describing the &lt;strong>runtime data areas&lt;/strong> — where objects physically live.&lt;/p></description><content:encoded><![CDATA[<h4 id="what-actually-happens-to-your-data-between-two-threadsand-the-small-set-of-tools-that-let-you-controlit">What actually happens to your data between two threads — and the small set of tools that let you control it</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/image-01.png"></p>
<p>Ask ten Java developers what “the Java Memory Model” means and you’ll get two different answers.</p>
<p>Half of them will draw a box called <em>Heap</em> and another called <em>Stack</em>. The other half will start talking about volatile and happens-before.</p>
<p>Both groups are talking about something real, but only the second group is talking about the actual <strong>Java Memory Model (JMM)</strong>. The first group is describing the <strong>runtime data areas</strong> — where objects physically live.</p>
<p>You need both to write correct concurrent code, so let’s go through them in order. Memory layout first, because it’s the easier one, then the JMM proper.</p>
<h3 id="part-1-where-your-datalives">Part 1: Where your data lives</h3>
<p>When the JVM starts, it carves the process memory into a few distinct regions. Each has an owner and a lifetime.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/image-02.png"></p>
<p><strong>Heap</strong> — every object and array you ever create lives here. It’s shared, which is exactly why concurrency is hard. Garbage collection happens here. Modern collectors split it further (young generation, old generation, or regions in G1/ZGC), but that’s a GC detail, not a memory model one.</p>
<p><strong>Metaspace</strong> — class metadata, bytecode, and static fields. It sits in native memory since Java 8, which is why PermGen errors disappeared and Metaspace OOM errors appeared instead.</p>
<p><strong>Thread stack</strong> — each thread gets its own. Every method call pushes a frame containing local variables, parameters, and the return address. Frame pops when the method returns. This is where StackOverflowError comes from. Nothing here is shared, so locals are never a concurrency problem — unless the <em>value</em> they point to is a shared object.</p>
<p><strong>PC register</strong> — the address of the instruction the thread is currently executing. One per thread.</p>
<p><strong>Native method stack</strong> — the same idea, but for native (JNI) calls.</p>
<p>One thing worth burning in:</p>
<pre tabindex="0"><code>Order order = new Order();
</code></pre><p>The variable order is a reference sitting on the <strong>stack</strong>. The Order object itself sits on the <strong>heap</strong>. Two threads can hold two separate stack references pointing to the same heap object. That&rsquo;s the whole source of the trouble.</p>
<h3 id="part-2-the-actual-java-memorymodel">Part 2: The actual Java Memory Model</h3>
<p>The runtime areas tell you <em>where</em> data sits. They tell you nothing about <em>when</em> one thread sees a write made by another.</p>
<p>That question is what the JMM answers. It’s a specification (JSR-133, in Chapter 17 of the Java Language Specification) that defines exactly what guarantees you get about reads and writes across threads.</p>
<h4 id="the-problem-it-exists-tosolve">The problem it exists to solve</h4>
<p>Modern CPUs are fast because they cheat, aggressively and correctly. Two things they do:</p>
<p><strong>1. Caching.</strong> Each core has its own L1/L2 cache. When a thread writes to a field, that write may sit in the core’s cache or store buffer for a while before it becomes visible to other cores.</p>
<p><strong>2. Reordering.</strong> The compiler, the JIT, and the CPU are all allowed to reorder instructions, as long as the result looks the same <em>for a single thread</em>. That last clause is the trap — nobody promised anything about how it looks to other threads.</p>
<p>So the JMM works with an abstraction:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/image-03.png"></p>
<p><em>Thread A and Thread B never see each other’s working memory. Everything they share has to travel through main memory — and the JMM decides when it does.</em></p>
<p>Every thread has a conceptual <strong>working memory</strong> (registers, cache, store buffers) holding copies of the variables it touches. Threads never talk to each other directly — they communicate only through <strong>main memory</strong>. The JMM specifies when a value must be flushed to main memory and when it must be re-read from it.</p>
<p>Here’s the classic demonstration:</p>
<pre tabindex="0"><code>public class StopFlag {
    private boolean running = true;   // no volatile
    
    public void run() {
        while (running) {
            // do work
        }
        System.out.println(&#34;stopped&#34;);
    }
    public void stop() {
        running = false;
    }
}
</code></pre><p>Thread A runs run(). Thread B calls stop(). Very often, thread A never stops.</p>
<p>Nothing is broken. The JIT sees that running is never modified inside the loop, so it&rsquo;s free to hoist the read out:</p>
<pre tabindex="0"><code>boolean r = running;
while (r) { /* work */ }   // now an infinite loop
</code></pre><p>Without a memory model rule forcing it, the JVM has no obligation to ever re-read that field. Your bug is not a race in the “two writes collided” sense — it’s a <strong>visibility</strong> failure.</p>
<h4 id="the-three-guarantees">The three guarantees</h4>
<p>Every concurrency problem in Java falls into one of these buckets.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/the-java-memory-model-explained-without-the-headache/image-04.png"></p>
<p>count++ is the textbook atomicity failure — it&rsquo;s three operations (read, add, write), and two threads can interleave in the middle. Two increments, one result.</p>
<h3 id="the-base-components-of-thejmm">The base components of the JMM</h3>
<p>Here’s the toolbox. It’s smaller than most people expect.</p>
<h4 id="1-happens-beforethe-rule-that-underpins-everything">1. happens-before — the rule that underpins everything</h4>
<p>Everything else is a special case of this one relation. If action <strong>A</strong> <em>happens-before</em> action <strong>B</strong>, then:</p>
<ul>
<li>everything A wrote is visible to B, and</li>
<li>A is guaranteed to appear to occur before B.</li>
</ul>
<p>It is <strong>not</strong> just “A ran earlier in wall-clock time.” Two actions can be ordered in time and still have no happens-before relationship, which means B may see stale data.</p>
<p>The rules the JLS gives you:</p>
<ol>
<li><strong>Program order</strong> — within one thread, each statement happens-before the ones written after it.</li>
<li><strong>Monitor lock</strong> — unlocking a monitor happens-before any later lock of the <em>same</em> monitor.</li>
<li><strong>volatile</strong> — a write to a volatile field happens-before every later read of that field.</li>
<li><strong>Thread start</strong> — Thread.start() happens-before anything in the started thread.</li>
<li><strong>Thread join</strong> — everything in a thread happens-before another thread’s successful join() on it.</li>
<li><strong>Interruption</strong> — a call to interrupt() happens-before the interrupted thread detects it.</li>
<li><strong>Constructor / finalizer</strong> — the end of a constructor happens-before the start of that object’s finalizer.</li>
<li><strong>Transitivity</strong> — if A hb B and B hb C, then A hb C.</li>
</ol>
<p>That last one does most of the practical work. It’s why writing a plain field and <em>then</em> a volatile flag safely publishes the plain field.</p>
<h4 id="2-volatilevisibility-and-ordering-no-atomicity">2. volatile — visibility and ordering, no atomicity</h4>
<pre tabindex="0"><code>private volatile boolean running = true;
</code></pre><p>This single keyword fixes the StopFlag example. It gives you:</p>
<ul>
<li><strong>Visibility</strong> — a write goes to main memory immediately; a read comes from main memory, never a cache.</li>
<li><strong>Ordering</strong> — the JVM inserts memory barriers so reads and writes can’t be reordered across the volatile access.</li>
</ul>
<p>What it does <strong>not</strong> give you is atomicity:</p>
<pre tabindex="0"><code>private volatile int count = 0;
public void increment() {
    count++;   // STILL BROKEN
}
</code></pre><p>Use volatile for flags and for single-writer state. For compound operations, use something else.</p>
<h4 id="3-synchronizedall-three-guarantees">3. synchronized — all three guarantees</h4>
<pre tabindex="0"><code>private int count = 0;
public synchronized void increment() {
    count++;   // correct
}
public synchronized int get() {
    return count;   // also needs to be synchronized
}
</code></pre><p>Entering the monitor gives mutual exclusion (atomicity). Exiting flushes to main memory, entering invalidates the local copy (visibility). Barriers around the block prevent reordering out of it.</p>
<p>The most common mistake here is synchronizing the writer but not the reader. Both sides need the <em>same</em> lock, or there’s no happens-before edge between them.</p>
<p>ReentrantLock gives you the same memory guarantees, plus tryLock, timeouts, fairness, and multiple conditions.</p>
<h4 id="4-finalsafe-publication-forfree"><strong>4.</strong> <strong>final — safe publication for free</strong></h4>
<pre tabindex="0"><code>public class Config {
    private final String url;
    public Config(String url) {
        this.url = url;   // guaranteed visible after construction
    }
}
</code></pre><p>The JMM guarantees that any thread that sees a properly constructed object sees the correct values of its final fields — no synchronization required. This is why immutable objects are the easiest possible concurrency strategy.</p>
<p>The catch is “properly constructed.” If the constructor leaks this (registering a listener, starting a thread), the guarantee is void.</p>
<h4 id="5-atomics-and-varhandle">5. Atomics and VarHandle</h4>
<p>java.util.concurrent.atomic gives you lock-free compound operations built on CPU compare-and-swap instructions:</p>
<pre tabindex="0"><code>private final AtomicInteger count = new AtomicInteger();
public void increment() {
    count.incrementAndGet();   // atomic, visible, ordered
}
</code></pre><p>Under contention LongAdder usually beats AtomicLong — it spreads writes across cells and sums them on read, avoiding cache-line contention. VarHandle (Java 9+) is the modern, supported way to reach for finer-grained modes like getAcquire / setRelease when you know exactly what you&rsquo;re doing.</p>
<h3 id="three-bugs-thisexplains">Three bugs this explains</h3>
<p><strong>Unsafe publication.</strong> Building an object and handing the reference to another thread without a happens-before edge. The other thread can see a non-null reference to a half-initialized object — fields still at their defaults. Publish through a volatile field, a lock, a final field, or a concurrent collection.</p>
<p><strong>Double-checked locking without</strong> <strong>volatile.</strong> The infamous one:</p>
<pre tabindex="0"><code>private static volatile Singleton instance;   // volatile is mandatory
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}
</code></pre><p>Drop the volatile and it breaks, because instance = new Singleton() is three steps — allocate, construct, assign — and the last two can be reordered. Another thread can grab a reference to an object whose constructor hasn&rsquo;t finished.</p>
<p><strong>Check-then-act.</strong> if (!map.containsKey(k)) map.put(k, v); is two atomic operations, not one atomic operation. Use putIfAbsent or computeIfAbsent.</p>
<h3 id="the-practical-cheatsheet">The practical cheat sheet</h3>
<ul>
<li><strong>Shared and mutable?</strong> It needs synchronization. Every time.</li>
<li><strong>One thread writes a flag, others read it?</strong> volatile.</li>
<li><strong>Compound operation on one variable?</strong> An atomic class.</li>
<li><strong>Compound operation across several variables?</strong> synchronized or ReentrantLock.</li>
<li><strong>Can you make it immutable instead?</strong> Do that. final fields, no setters, no shared mutation, no memory model to reason about.</li>
<li><strong>Reading is also access.</strong> Unsynchronized reads of synchronized state are still broken.</li>
<li><strong>“It works on my machine” proves nothing.</strong> x86 has a strong memory model that hides many of these bugs. ARM (Apple Silicon, Graviton) doesn’t. Code that ran fine for years on Intel servers can fail the week you move it to ARM instances.</li>
</ul>
<h3 id="wrapping-up">Wrapping up</h3>
<p>The runtime areas — heap, stacks, metaspace — tell you where your data sits. The Java Memory Model tells you when one thread’s writes become visible to another, and it comes down to a handful of pieces: happens-before as the underlying rule, and volatile, synchronized, final, and the atomics as the tools that create it.</p>
<p>You don’t need to memorize the specification. You need one habit: whenever two threads touch the same data, ask what creates the happens-before edge between them. If you can’t name it, it isn’t there.</p>
<p><em>If this was useful, the natural next step is Brian Goetz’s</em> Java Concurrency in Practice <em>— still the best treatment of this material, twenty years on.</em></p>
]]></content:encoded></item><item><title>Your DELETE + INSERT Is Burning PostgreSQL’s CPU. Here’s What’s Actually Happening on the Page.</title><link>https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/</link><pubDate>Mon, 17 Aug 2026 18:56:51 +0000</pubDate><guid>https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/</guid><description>&lt;p>&lt;em>A deep dive into MVCC, heap pages, vacuum, and what to do when you have billions of rows to update.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-01.png">&lt;/p>
&lt;h3 id="the-symptom">The symptom&lt;/h3>
&lt;p>You have a table that stores user state. Something happens — an event arrives — and your code does the obvious thing:&lt;/p>
&lt;pre tabindex="0">&lt;code>BEGIN;
DELETE FROM user_state WHERE user_id = $1;
INSERT INTO user_state (user_id, state, score, updated_at) VALUES ($1, $2, $3, now());
COMMIT;
&lt;/code>&lt;/pre>&lt;p>It’s clean. It’s idempotent. It doesn’t require you to think about which columns changed. For a long time it works fine.&lt;/p></description><content:encoded><![CDATA[<p><em>A deep dive into MVCC, heap pages, vacuum, and what to do when you have billions of rows to update.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-01.png"></p>
<h3 id="the-symptom">The symptom</h3>
<p>You have a table that stores user state. Something happens — an event arrives — and your code does the obvious thing:</p>
<pre tabindex="0"><code>BEGIN;
DELETE FROM user_state WHERE user_id = $1;
INSERT INTO user_state (user_id, state, score, updated_at) VALUES ($1, $2, $3, now());
COMMIT;
</code></pre><p>It’s clean. It’s idempotent. It doesn’t require you to think about which columns changed. For a long time it works fine.</p>
<p>Then event volume grows, and PostgreSQL’s CPU starts climbing in a way that nothing in your query plans explains. Your queries are still index scans. Your pg_stat_statements top entries look reasonable. But CPU is pinned, replication lag creeps up, and every so often the whole database gets slow for a few minutes and then recovers on its own.</p>
<p>If you look at pg_stat_activity during one of those windows, you&rsquo;ll usually find the culprit sitting right there:</p>
<pre tabindex="0"><code>autovacuum: VACUUM public.user_state
</code></pre><p>That’s the story. But “autovacuum is expensive” is not an explanation — it’s a restatement of the symptom. To actually fix this you need to understand what a DELETE followed by an INSERT does to the physical 8 KB pages on disk, and why that specific shape of write is close to the worst case for PostgreSQL&rsquo;s storage engine.</p>
<p>Let’s go all the way down.</p>
<h3 id="part-1-postgresql-never-modifies-a-row-inplace">Part 1: PostgreSQL never modifies a row in place</h3>
<p>This is the single fact that everything else follows from.</p>
<p>PostgreSQL implements MVCC (Multi-Version Concurrency Control) by keeping <strong>multiple physical versions of every row</strong> and letting each transaction see the version appropriate to its snapshot. There is no undo log (unlike Oracle or MySQL’s InnoDB, which keep old versions in a separate rollback segment). In PostgreSQL, old row versions live <strong>in the table itself</strong>, mixed in with the live ones.</p>
<p>Concretely:</p>
<ul>
<li>INSERT writes a new tuple and stamps it with xmin = the inserting transaction ID.</li>
<li>DELETE <strong>does not remove anything</strong>. It stamps the existing tuple with xmax = the deleting transaction ID. The bytes stay exactly where they were.</li>
<li>UPDATE is internally a DELETE + INSERT: it stamps xmax on the old tuple and writes a brand-new tuple somewhere.</li>
</ul>
<p>So a “deleted” row is still fully present on disk, occupying its bytes, until some later process comes along and physically reclaims it. That process is vacuum.</p>
<p>Here’s this happening for real. A 100-row table, then delete 50 rows and insert 50 new ones:</p>
<pre tabindex="0"><code>CREATE EXTENSION pageinspect;
CREATE TABLE demo (id int PRIMARY KEY, state text);
INSERT INTO demo SELECT g, &#39;active&#39; FROM generate_series(1,100) g;
-- delete half, re-insert half (the pattern from our app)
DELETE FROM demo WHERE id &lt;= 50;
INSERT INTO demo SELECT g, &#39;active&#39; FROM generate_series(1,50) g;
SELECT lp, lp_flags, lp_len, t_xmin, t_xmax
FROM heap_page_items(get_raw_page(&#39;demo&#39;, 0))
WHERE lp BETWEEN 48 AND 54;
</code></pre><pre tabindex="0"><code> lp | lp_flags | lp_len | t_xmin | t_xmax
----+----------+--------+--------+--------
 48 |        1 |     35 |    801 |    802   &lt;- deleted, still on the page
 49 |        1 |     35 |    801 |    802   &lt;- deleted, still on the page
 50 |        1 |     35 |    801 |    802   &lt;- deleted, still on the page
 51 |        1 |     35 |    801 |      0   &lt;- live
 52 |        1 |     35 |    801 |      0   &lt;- live
 53 |        1 |     35 |    801 |      0   &lt;- live
 54 |        1 |     35 |    801 |      0   &lt;- live
</code></pre><p>The deleted tuples have t_xmax = 802 (the transaction that deleted them) and are still LP_NORMAL (lp_flags = 1). They occupy real space. The table now has 150 line pointers where the logical row count is 100.</p>
<pre tabindex="0"><code>SELECT tuple_count, dead_tuple_count, dead_tuple_percent, free_percent
FROM pgstattuple(&#39;demo&#39;);
</code></pre><pre tabindex="0"><code> tuple_count | dead_tuple_count | dead_tuple_percent | free_percent
-------------+------------------+--------------------+--------------
         100 |               50 |              21.36 |        19.09
</code></pre><p><strong>21% of that table is garbage.</strong> And that’s after one round.</p>
<h3 id="part-2-anatomy-of-an-8-kbpage">Part 2: Anatomy of an 8 KB page</h3>
<p>PostgreSQL stores everything in fixed-size blocks — 8 KB by default (SHOW block_size). Understanding the layout of one page is the key to understanding why churn is expensive.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-02.png"></p>
<p>Two structures grow toward each other from opposite ends: the <strong>line pointer array</strong> from the top, the <strong>tuple data</strong> from the bottom. The page is full when they meet.</p>
<p>The <strong>line pointer</strong> (also called an item identifier, ItemIdData) is a tiny 4-byte struct that says &ldquo;the tuple at slot N lives at byte offset X and is Y bytes long.&rdquo; It has four possible states, and you&rsquo;ll see all of them in this article:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-03.png"></p>
<p>The <strong>tuple header</strong> (HeapTupleHeader, 23 bytes padded to 24) is where MVCC lives:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-04.png"></p>
<p>That’s <strong>24 bytes of overhead per row version, plus 4 bytes for the line pointer.</strong> For a table storing a user ID, a short state string, and a timestamp, the MVCC bookkeeping can easily be 40% of the row’s on-disk size.</p>
<p>Now here’s the critical bit for our problem:</p>
<blockquote>
<p><em>A page’s free space is only usable for a</em> new <em>tuple if it is</em> <em><strong>contiguous</strong></em> <em>and the page has a free line pointer or room to add one. Dead tuples fragment the page. Only vacuum can defragment it.</em></p></blockquote>
<h3 id="part-3-why-delete--insert-is-the-worstshape">Part 3: Why DELETE + INSERT is the worst shape</h3>
<p>Compare what the two write patterns do to a page.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-05.png"></p>
<p>A DELETE + INSERT can <strong>never</strong> be a HOT update. HOT (Heap-Only Tuple) is an optimization that only applies to UPDATE, and only when two conditions hold:</p>
<ol>
<li>No indexed column changed.</li>
<li>The new version fits <strong>on the same page</strong> as the old one.</li>
</ol>
<p>When both hold, PostgreSQL writes the new version onto the same page and links it via t_ctid, leaving the index entries alone. That&rsquo;s an enormous saving: index maintenance is usually the dominant cost of a write, because B-tree inserts are random-access, and every index on the table pays it.</p>
<p>By splitting your logical update into two statements, you’ve told PostgreSQL: <em>this is a brand-new row that has nothing to do with the old one.</em> It has no choice but to write full index entries into every index, and to leave a dead tuple behind that vacuum must chase down.</p>
<p>You can see HOT working with pageinspect. In a table where updates <em>are</em> HOT-eligible, the page fills up with redirect pointers:</p>
<pre tabindex="0"><code>SELECT lp, lp_flags, lp_len, t_ctid
FROM heap_page_items(get_raw_page(&#39;user_state_hot&#39;, 0)) LIMIT 5;
</code></pre><pre tabindex="0"><code> lp | lp_flags | lp_len | t_ctid
----+----------+--------+--------
  1 |        2 |      0 |          &lt;- LP_REDIRECT
  2 |        2 |      0 |          &lt;- LP_REDIRECT
  3 |        2 |      0 |          &lt;- LP_REDIRECT
  4 |        2 |      0 |          &lt;- LP_REDIRECT
  5 |        2 |      0 |          &lt;- LP_REDIRECT
</code></pre><p>Those redirect pointers are the physical trace of updates that cost <strong>zero index writes</strong>.</p>
<h3 id="part-4-what-vacuum-actuallydoes">Part 4: What vacuum actually does</h3>
<p>Vacuum is not one operation. A standard (non-full) VACUUM of a table with indexes runs in phases, and understanding the phases explains where the CPU goes.</p>
<h4 id="phase-1heapscan">Phase 1 — Heap scan</h4>
<p>Vacuum scans the table’s pages (skipping ones the <strong>visibility map</strong> marks all-visible, which is the main reason vacuum on an append-only table is cheap and vacuum on a churned table is not). For each page it:</p>
<ul>
<li>Prunes HOT chains and marks reclaimable tuples.</li>
<li>Collects the <strong>TIDs (page number, line pointer) of dead tuples</strong> into memory.</li>
<li>Sets LP_DEAD on their line pointers.</li>
</ul>
<p>The dead TID list is bounded by maintenance_work_mem (or autovacuum_work_mem). In PostgreSQL 17+ this list is stored in an adaptive radix tree (TidStore) rather than a flat array, which cut memory usage dramatically and removed the old 1 GB cap — a genuinely large improvement for high-churn tables.</p>
<h4 id="phase-2indexvacuum">Phase 2 — Index vacuum</h4>
<p><strong>This is the expensive part.</strong> For every index on the table, vacuum performs a full scan (a btbulkdelete) and removes every index entry pointing at a dead TID.</p>
<p>Note what that means: <strong>the cost is per index, and it’s a full index scan, not a lookup.</strong> A table with five indexes pays five full index scans per vacuum cycle. This is why “just add an index” decisions compound badly on high-churn tables.</p>
<h4 id="phase-3heapvacuum">Phase 3 — Heap vacuum</h4>
<p>Vacuum revisits the pages and turns LP_DEAD line pointers into LP_UNUSED, then <strong>defragments</strong> the page: it slides the remaining tuples together so the free space becomes one contiguous block. Now that space is genuinely reusable.</p>
<p>If the dead-TID list overflowed maintenance_work_mem in phase 1, vacuum has to loop: scan more heap → vacuum all indexes again → vacuum heap again. A single vacuum of a big churned table can do the full index pass <strong>multiple times</strong>. This is the classic &ldquo;why has autovacuum been running for six hours&rdquo; answer.</p>
<h4 id="phase-4freezing">Phase 4 — Freezing</h4>
<p>Separately, vacuum replaces old xmin values with a frozen marker so transaction IDs can wrap around safely. Anti-wraparound vacuums are non-cancelable and will run whether you like it or not. PostgreSQL 18 added <em>eager freezing</em> (vacuum_max_eager_freeze_failure_rate, default 0.03) so that normal vacuums opportunistically freeze pages, spreading this cost out instead of saving it up for one giant emergency vacuum.</p>
<h4 id="phase-5truncation">Phase 5 — Truncation</h4>
<p>If there are empty pages at the <em>end</em> of the table, vacuum tries to return them to the OS. This requires an ACCESS EXCLUSIVE lock (briefly). Note the word <em>end</em>: <strong>vacuum almost never shrinks a bloated table</strong>, because the free space is scattered throughout, not conveniently at the tail. It makes the space reusable by PostgreSQL, not available to your filesystem.</p>
<p>Here’s the before/after on our demo table:</p>
<pre tabindex="0"><code>VACUUM demo;
SELECT lp_flags, count(*) 
FROM heap_page_items(get_raw_page(&#39;demo&#39;,0)) GROUP BY 1;
</code></pre><p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-06.png"></p>
<p>Free space on the page went from <strong>19.09% → 43.51%</strong>. That’s the work vacuum did. Now multiply it by every page you churned.</p>
<h3 id="part-5-the-amplification-math">Part 5: The amplification math</h3>
<p>Here is the part people underestimate. One logical “user state changed” event does not cause one write. It causes, at minimum:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-07.png"></p>
<p>Steps 7–13 are <em>invisible in your application metrics</em>. Your app measures step 1–6 latency and reports “the insert took 2 ms.” The other two-thirds of the cost lands on a background worker, asynchronously, in a burst, on the same CPU.</p>
<p>That’s the answer to “why does CPU go up but my queries look fine.”</p>
<h4 id="point-5b-the-full-page-image-trap"><strong>Point 5b: the full-page-image trap</strong></h4>
<p>After each checkpoint, the first write to any page triggers a <strong>full 8 KB page image</strong> written into WAL (full_page_writes, on by default, and required for crash safety on most filesystems). If your churn touches many distinct pages, you pay a lot of FPIs. If your churn is concentrated on a few pages, you pay far fewer. Delete+insert scatters writes across pages — the new tuple often lands in a <em>different</em> page from the one you dirtied with the xmax write. Two dirty pages instead of one, two FPI candidates instead of one.</p>
<h4 id="point-5c-the-read-side-cost-nobodymentions">Point 5c: the read-side cost nobody mentions</h4>
<p>Dead tuples also make <strong>reads</strong> slower and CPU-hungrier:</p>
<ul>
<li>An index scan follows an index entry to the heap, reads the tuple, checks visibility, discovers it’s dead, and discards it. That’s a wasted buffer access plus a visibility check. You can see this directly: EXPLAIN (ANALYZE, BUFFERS) will show a rising Buffers: shared hit count for a query returning the same number of rows.</li>
<li>Bloated tables have worse cache locality. The same 100k live rows spread over 2× the pages means 2× the shared buffers to hold them, which evicts <em>other</em> things.</li>
<li>On a read, PostgreSQL may perform <strong>opportunistic HOT pruning</strong> on the page — real CPU work done inside your SELECT.</li>
</ul>
<h3 id="part-6-measurements">Part 6: Measurements</h3>
<p>I ran a controlled comparison: 100,000 rows, ten rounds of modifying 20,000 of them, with an explicit VACUUM after each round so we&rsquo;re looking at steady state, not a runaway. Three variants:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/your-delete-insert-is-burning-postgresqls-cpu-here-s-what-s-actually-happening-on-the-page/image-08.png"></p>
<p>Read those rows carefully, because the middle one is the trap.</p>
<p><strong>Switching from</strong> **DELETE+**<strong>INSERT to</strong> <strong>UPDATE bought essentially nothing on its own.</strong> The second variant still generated <em>more</em> WAL than the first. Why? Because the UPDATE touched updated_at, and updated_at had an index on it. An UPDATE that modifies any indexed column is not HOT-eligible, so it does exactly what delete+insert does: new heap tuple, new entries in every index, dead tuple left behind.</p>
<p>The third variant — same UPDATE, but with no index on the mutated columns and fillfactor lowered to 80 — generated <strong>less than half the WAL</strong> and made 73% of its updates HOT.</p>
<p>The cost of that win is visible too: the HOT table starts at 1042 heap pages instead of 834, because fillfactor=80 deliberately leaves 20% of every page empty. That empty space is what new row versions land in. <strong>You are trading disk space for CPU and I/O</strong>, which on modern hardware is almost always the right trade.</p>
<p><em>(These are synthetic single-client numbers on PostgreSQL 16 — treat the ratios as directional, not as a benchmark of your workload.)</em></p>
<h3 id="part-7-fixing-it-in-order-ofleverage">Part 7: Fixing it, in order of leverage</h3>
<h4 id="71-dont-write-when-nothingchanged">7.1 Don’t write when nothing changed</h4>
<p>The cheapest write is the one that doesn’t happen. A shocking amount of churn in “collect user state” systems is <strong>rewriting a row to the exact same value</strong> because an event arrived that didn’t actually change anything.</p>
<pre tabindex="0"><code>INSERT INTO user_state (user_id, state, score, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (user_id) DO UPDATE
SET state      = EXCLUDED.state,
    score      = EXCLUDED.score,
    updated_at = now()
WHERE user_state.state  IS DISTINCT FROM EXCLUDED.state
   OR user_state.score  IS DISTINCT FROM EXCLUDED.score;
</code></pre><p>That trailing WHERE on the DO UPDATE clause is the whole trick: if nothing changed, PostgreSQL writes <strong>nothing</strong> — no new tuple, no dead tuple, no index entry, no WAL. In event-driven systems where the same state gets recomputed repeatedly, this alone often removes 50–90% of write volume.</p>
<p>Measure how much you’d save before you build anything else:</p>
<pre tabindex="0"><code>-- how many of your writes are no-ops?
SELECT count(*) FILTER (WHERE state = $2 AND score = $3) AS would_be_noop,
       count(*) AS total
FROM user_state WHERE user_id = ANY($1);
</code></pre><h4 id="72-make-your-updates-hot-eligible">7.2 Make your updates HOT-eligible</h4>
<p>Two requirements, both actionable:</p>
<p><strong>(a) Don’t index the columns that change.</strong> Audit every index against your write path:</p>
<pre tabindex="0"><code>SELECT i.relname AS index_name,
       array_agg(a.attname ORDER BY x.n) AS indexed_columns,
       s.idx_scan AS times_used
FROM pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_class t ON t.oid = ix.indrelid
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS x(attnum, n) ON true
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
LEFT JOIN pg_stat_user_indexes s ON s.indexrelid = i.oid
WHERE t.relname = &#39;user_state&#39;
GROUP BY i.relname, s.idx_scan
ORDER BY s.idx_scan NULLS FIRST;
</code></pre><p>An index on updated_at is the single most common HOT-killer in exactly this kind of table. Ask whether you truly need it, or whether a partial index, a coarser index (date_trunc(&lsquo;hour&rsquo;, updated_at) doesn&rsquo;t help — but a separate low-churn audit table might), or no index at all would do.</p>
<p>Unused indexes are pure loss: they cost writes and vacuum passes and return nothing. Drop them.</p>
<p><strong>(b) Leave room on the page.</strong></p>
<pre tabindex="0"><code>ALTER TABLE user_state SET (fillfactor = 80);
-- fillfactor only applies to newly written pages;
-- rewrite the table to apply it to existing data:
VACUUM FULL user_state;   -- takes ACCESS EXCLUSIVE lock
-- or, online:
-- pg_repack -t user_state
</code></pre><p>For a heavily-updated table, 70–85 is a reasonable range. Below ~70 you’re wasting a lot of buffer cache on empty space.</p>
<p>Then confirm it’s working:</p>
<pre tabindex="0"><code>SELECT relname,
       n_tup_upd,
       n_tup_hot_upd,
       round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd,0), 1) AS hot_pct
FROM pg_stat_user_tables
WHERE relname = &#39;user_state&#39;;
</code></pre><p><strong>If</strong> <strong>hot_pct is near zero on an update-heavy table, that is your bug.</strong> Aim for 80%+.</p>
<h4 id="73-tune-autovacuum-for-this-table-notglobally">7.3 Tune autovacuum for this table, not globally</h4>
<p>The defaults are tuned for a small table on a small machine. autovacuum_vacuum_scale_factor = 0.2 means vacuum waits until <strong>20% of the table is dead</strong> before acting. On a 500-million-row table that&rsquo;s 100 million dead tuples before anything happens — and then one enormous, disruptive vacuum.</p>
<p>You want the opposite: vacuum <strong>more often, doing less each time.</strong></p>
<pre tabindex="0"><code>ALTER TABLE user_state SET (
  autovacuum_vacuum_scale_factor  = 0.01,   -- act at 1% dead, not 20%
  autovacuum_vacuum_threshold     = 1000,
  autovacuum_analyze_scale_factor = 0.02,
  autovacuum_vacuum_cost_delay    = 0,      -- don&#39;t throttle this table
  autovacuum_vacuum_cost_limit    = 3000
);
</code></pre><p>And globally, if your hardware is modern (NVMe, many cores), the shipped throttle is far too conservative:</p>
<pre tabindex="0"><code>autovacuum_max_workers      = 6          # default 3
autovacuum_naptime          = 15s        # default 1min
autovacuum_vacuum_cost_delay = 2ms       # default 2ms; consider 0 on fast storage
vacuum_cost_limit           = 2000       # default 200
maintenance_work_mem        = 2GB        # gives vacuum a big dead-TID store
autovacuum_work_mem         = 1GB
</code></pre><p>The cost-delay mechanism is the one most often left at a value that makes vacuum unable to keep up: vacuum accumulates “cost” (1 per buffer hit, 2 per miss, 20 per dirtied page) and sleeps cost_delay whenever it exceeds cost_limit. With the defaults, vacuum is deliberately slowed to something like a few MB/s. On a table generating hundreds of MB of dead tuples per minute, it will never catch up, and dead tuples accumulate forever.</p>
<p>PostgreSQL 18 also added autovacuum_vacuum_max_threshold (default 100,000,000), which caps the scale-factor calculation so very large tables can&rsquo;t defer vacuum indefinitely. It helps, but a per-table scale factor is still better.</p>
<h4 id="74-reconsider-the-datamodel">7.4 Reconsider the data model</h4>
<p>If your state genuinely is “replace the whole thing on every event,” ask whether it belongs in a row-per-user table at all:</p>
<ul>
<li><strong>Collapse many rows into one.</strong> If you’re storing N state rows per user and replacing them all, one row with a jsonb column can turn N deletes + N inserts into one update — and if jsonb is under the TOAST threshold and no indexed column changes, that update can be HOT. (Careful: a large jsonb that gets TOASTed is a whole separate write amplification problem. This works when the document is small, roughly under 2 KB.)</li>
<li><strong>Append-only + a materialized “current” view.</strong> Write events to a partitioned append-only table (no updates, no deletes, so almost no vacuum work — just insert-triggered vacuums for freezing), and maintain the current state separately, or compute it with a DISTINCT ON query over a well-indexed event log. Append-only tables are the cheapest thing PostgreSQL does.</li>
<li><strong>Move the hottest state out of PostgreSQL.</strong> If a piece of state changes many times per second per user and you only ever need the latest value, Redis or a dedicated KV store is the honest answer. Not everything needs to be transactional and durable.</li>
</ul>
<h3 id="part-8-the-hard-questionupdating-billions-ofrows">Part 8: The hard question — updating billions of rows</h3>
<p>This is the case where all the usual advice breaks, so it deserves its own treatment.</p>
<h4 id="why-the-naive-approach-fails-precisely">Why the naive approach fails, precisely</h4>
<pre tabindex="0"><code>UPDATE huge_table SET status = &#39;migrated&#39; WHERE status = &#39;pending&#39;;
-- 2,000,000,000 rows
</code></pre><p>Four separate disasters:</p>
<p><strong>1. Table size doubles.</strong> Every updated row writes a new version while the old one stays. A 2 TB table becomes 4 TB mid-statement. Then vacuum makes the space reusable but does <em>not</em> return it to the OS. You now permanently need 4 TB of disk for 2 TB of data.</p>
<p><strong>2. It generates terabytes of WAL</strong> in one burst. Replicas fall behind, archiving falls behind, and if you’re on a cloud provider you may fill the WAL volume and take the primary down.</p>
<p><strong>3. It holds a single transaction open for hours — and this is the one that hurts most.</strong> Vacuum can only remove tuples older than the oldest running transaction’s snapshot (the <em>xmin horizon</em>). A long transaction pins that horizon. So while your giant UPDATE runs, <strong>vacuum cannot clean up dead tuples anywhere in the entire database</strong> — not just this table. Every other table quietly bloats. This is how one migration takes down a whole cluster.</p>
<p><strong>4. If it fails at 95%, all the work is thrown away</strong> — but the dead tuples it created are not. You get the bloat with none of the benefit.</p>
<p>You can watch the horizon problem happen:</p>
<pre tabindex="0"><code>SELECT pid, age(backend_xid) AS xid_age, state,
       now() - xact_start AS duration, left(query, 60)
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
ORDER BY xact_start
LIMIT 5;
</code></pre><p>Also check for the other two horizon-pinners people forget: <strong>replication slots</strong> and <strong>prepared transactions</strong>.</p>
<pre tabindex="0"><code>SELECT slot_name, active, age(xmin) AS xmin_age,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
SELECT gid, prepared, age(transaction) FROM pg_prepared_xacts;
</code></pre><p>An inactive replication slot will pin the xmin horizon <strong>forever</strong> and no amount of vacuum tuning will save you.</p>
<h4 id="strategy-a-batched-backfill-with-keyset-pagination">Strategy A: Batched backfill with keyset pagination</h4>
<p>The workhorse. Small transactions, each one committing so the xmin horizon advances and vacuum can keep pace.</p>
<pre tabindex="0"><code>-- Track progress in a real table so the job is resumable
CREATE TABLE IF NOT EXISTS backfill_progress (
  job_name text PRIMARY KEY,
  last_id  bigint NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now()
);
</code></pre><pre tabindex="0"><code>-- One batch. Run this in a loop from your application or a scheduler.
WITH bounds AS (
  SELECT last_id FROM backfill_progress WHERE job_name = &#39;status_migration&#39;
),
batch AS (
  SELECT id FROM huge_table
  WHERE id &gt; (SELECT last_id FROM bounds)
  ORDER BY id
  LIMIT 5000
  FOR UPDATE SKIP LOCKED
),
upd AS (
  UPDATE huge_table t
  SET status = &#39;migrated&#39;
  FROM batch b
  WHERE t.id = b.id
    AND t.status IS DISTINCT FROM &#39;migrated&#39;   -- skip no-op writes
  RETURNING t.id
)
UPDATE backfill_progress
SET last_id = COALESCE((SELECT max(id) FROM batch), last_id),
    updated_at = now()
WHERE job_name = &#39;status_migration&#39;
RETURNING last_id, (SELECT count(*) FROM upd) AS rows_updated;
</code></pre><p>Rules that make the difference between this working and this being a slow disaster:</p>
<ul>
<li><strong>Keyset pagination, never</strong> <strong>OFFSET.</strong> OFFSET 10000000 re-scans ten million rows every batch. WHERE id &gt; $last uses the index.</li>
<li><strong>Batch size 1,000–10,000 rows.</strong> Big enough to amortize overhead, small enough that each transaction finishes in well under a second.</li>
<li><strong>AND col IS DISTINCT FROM $new</strong> so re-running a batch is free and no-ops don&rsquo;t write.</li>
<li><strong>Throttle deliberately.</strong> Sleep 50–200 ms between batches. You are racing autovacuum; give it room to win. Adaptive throttling — sleep longer when replication lag or dead-tuple count rises — is worth the extra code:</li>
</ul>
<pre tabindex="0"><code>-- feed this into your loop&#39;s sleep decision
SELECT (SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname=&#39;huge_table&#39;) AS dead,
       (SELECT coalesce(max(pg_wal_lsn_diff(sent_lsn, replay_lsn)), 0)
        FROM pg_stat_replication) AS replica_lag_bytes;
</code></pre><ul>
<li><strong>Run it during a low-traffic window</strong> if you have one, but design it to be safe at any time.</li>
</ul>
<p><strong>Expected duration is the thing to be honest about:</strong> 2 billion rows at 5,000 rows/batch with a 100 ms sleep is 400,000 batches ≈ <strong>11+ hours minimum</strong>, realistically a few days. That’s fine. Plan for days, make it resumable, make it observable.</p>
<h4 id="strategy-b-partitioning--partition-swap">Strategy B: Partitioning + partition swap</h4>
<p>If your data is naturally partitionable (by time, tenant, hash of user ID), this changes the game completely, because you can rebuild a partition <strong>offline</strong> and swap it in atomically.</p>
<pre tabindex="0"><code>-- Assume: huge_table PARTITION BY RANGE (created_at)
--         with partition huge_table_2026_03
-- 1. Build the new version at full speed, outside the live table.
CREATE TABLE huge_table_2026_03_new (LIKE huge_table INCLUDING ALL);
INSERT INTO huge_table_2026_03_new
SELECT id, user_id, &#39;migrated&#39;::text AS status, payload, created_at
FROM huge_table_2026_03;
-- 2. Indexes AFTER the load - far faster than maintaining them during it.
--    No CONCURRENTLY needed: nothing is reading this table yet.
CREATE INDEX ON huge_table_2026_03_new (user_id);
ANALYZE huge_table_2026_03_new;
-- 3. Atomic swap: milliseconds of ACCESS EXCLUSIVE lock, not hours.
BEGIN;
  ALTER TABLE huge_table DETACH PARTITION huge_table_2026_03;
  ALTER TABLE huge_table ATTACH PARTITION huge_table_2026_03_new
    FOR VALUES FROM (&#39;2026-03-01&#39;) TO (&#39;2026-04-01&#39;);
COMMIT;
-- 4. Keep the old one around until you&#39;re confident, then drop it.
-- DROP TABLE huge_table_2026_03;
</code></pre><p>Why this is so much better:</p>
<ul>
<li>The bulk INSERT into an empty, unindexed table is <strong>the fastest write path PostgreSQL has</strong>. No dead tuples are created at all.</li>
<li>Zero vacuum debt: you never made garbage, so there’s nothing to collect.</li>
<li>The lock window is milliseconds.</li>
<li>Rollback is trivial: swap the old partition back.</li>
<li>You can do partitions <strong>one at a time</strong>, over weeks, with a rollback point after each.</li>
</ul>
<p>There’s a caveat worth knowing: ATTACH PARTITION validates that existing rows satisfy the partition constraint, which requires a scan — unless you add a matching CHECK constraint to the new table first, in which case PostgreSQL skips the scan.</p>
<pre tabindex="0"><code>ALTER TABLE huge_table_2026_03_new
  ADD CONSTRAINT part_check
  CHECK (created_at &gt;= &#39;2026-03-01&#39; AND created_at &lt; &#39;2026-04-01&#39;) NOT VALID;
ALTER TABLE huge_table_2026_03_new VALIDATE CONSTRAINT part_check;
-- now ATTACH is instant
</code></pre><p><strong>If you have billions of rows and no partitioning, partitioning is probably the actual project</strong> — not the update itself. Everything downstream gets easier: DROP PARTITION replaces mass DELETE (instant, and creates zero dead tuples), vacuum works per-partition in parallel, and index sizes stay manageable.</p>
<h4 id="strategy-c-rebuild-and-swap-the-wholetable">Strategy C: Rebuild and swap the whole table</h4>
<p>When the change is uniform and you can’t partition, build a new table and cut over.</p>
<p>For a bloated table with no schema change, pg_repack does this online: it builds a fresh copy, captures concurrent changes via triggers, replays them, and swaps — taking the exclusive lock only for the final instant.</p>
<pre tabindex="0"><code>pg_repack -h host -d mydb -t huge_table --jobs 4
</code></pre><p>For a schema/data change, the same idea by hand with logical replication or triggers; tools like pgroll and Percona&rsquo;s approach automate it. The pattern is always: <strong>new table, bulk load, keep in sync, atomic rename.</strong></p>
<p>Budget for double the disk space. That’s the price of every swap-based strategy.</p>
<h3 id="strategy-d-dont-update-atall">Strategy D: Don’t update at all</h3>
<p>The strategy people skip past. Ask whether the update is necessary:</p>
<ul>
<li>Add a <strong>new column with a default</strong> instead of rewriting values — since PostgreSQL 11, adding a column with a non-volatile default is a metadata-only change that rewrites nothing, no matter how big the table.</li>
<li>Write a <strong>view</strong> that computes the new value from the old one, and migrate readers to it.</li>
<li>Use a <strong>generated column</strong> if the new value is derivable.</li>
<li>Let it happen <strong>lazily</strong>: update rows as they’re naturally touched by the application, with a background job mopping up the long tail over months.</li>
</ul>
<p>A billion-row update you don’t have to run is strictly better than one you run well.</p>
<h3 id="part-9-what-to-actuallywatch">Part 9: What to actually watch</h3>
<p>Four queries. Put them on a dashboard.</p>
<p><strong>1. Is vacuum keeping up?</strong></p>
<pre tabindex="0"><code>SELECT relname,
       n_live_tup,
       n_dead_tup,
       round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
       last_autovacuum,
       autovacuum_count,
       n_tup_upd,
       n_tup_hot_upd,
       round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0), 1) AS hot_pct
FROM pg_stat_user_tables
WHERE n_dead_tup &gt; 10000
ORDER BY n_dead_tup DESC
LIMIT 20;
</code></pre><p>Sustained dead_pct above ~10% on a busy table means vacuum is losing. hot_pct near zero on an update-heavy table means you&rsquo;re paying full index cost on every write.</p>
<p><strong>2. What is vacuum doing right now?</strong></p>
<pre tabindex="0"><code>SELECT p.pid, p.relid::regclass AS table, p.phase,
       pg_size_pretty(p.heap_blks_total * 8192::bigint) AS total,
       round(100.0 * p.heap_blks_scanned / NULLIF(p.heap_blks_total,0), 1) AS pct_scanned,
       p.index_vacuum_count,
       now() - a.xact_start AS running_for
FROM pg_stat_progress_vacuum p
JOIN pg_stat_activity a USING (pid);
</code></pre><p>index_vacuum_count &gt; 1 is the smoking gun for maintenance_work_mem being too small — vacuum had to do the index pass more than once.</p>
<p><strong>3. Is anything pinning the xmin horizon?</strong></p>
<pre tabindex="0"><code>SELECT &#39;backend&#39; AS src, pid::text AS id, age(backend_xmin) AS xmin_age
FROM pg_stat_activity WHERE backend_xmin IS NOT NULL
UNION ALL
SELECT &#39;slot&#39;, slot_name, age(xmin) FROM pg_replication_slots WHERE xmin IS NOT NULL
UNION ALL
SELECT &#39;prepared_xact&#39;, gid, age(transaction) FROM pg_prepared_xacts
ORDER BY xmin_age DESC NULLS LAST
LIMIT 10;
</code></pre><p>Anything with a large age here is preventing vacuum from cleaning up <strong>database-wide.</strong></p>
<p><strong>4. How bloated is it, really?</strong></p>
<pre tabindex="0"><code>CREATE EXTENSION IF NOT EXISTS pgstattuple;
-- accurate but does a full scan; use the sampling version on huge tables
SELECT * FROM pgstattuple(&#39;user_state&#39;);
SELECT * FROM pgstattuple_approx(&#39;huge_table&#39;);
SELECT * FROM pgstatindex(&#39;user_state_user_id_idx&#39;);
</code></pre><p>For indexes, avg_leaf_density below ~50 means the index is worth rebuilding:</p>
<pre tabindex="0"><code>REINDEX INDEX CONCURRENTLY user_state_user_id_idx;
</code></pre><h3 id="the-checklist">The checklist</h3>
<p>If you’re in the situation this article opened with, do these in order:</p>
<ol>
<li><strong>Measure first.</strong> dead_pct, hot_pct, pg_stat_progress_vacuum, xmin horizon. Don&rsquo;t tune blind.</li>
<li><strong>Kill no-op writes</strong> with ON CONFLICT DO UPDATE &hellip; WHERE &hellip; IS DISTINCT FROM. Usually the biggest single win, and the cheapest to ship.</li>
<li><strong>Convert delete+insert to</strong> <strong>UPDATE</strong> — but only after step 4, or it buys you nothing.</li>
<li><strong>Make updates HOT-eligible.</strong> Drop or move indexes on frequently-changed columns (especially updated_at). Set fillfactor to 80. Verify hot_pct climbs above 80%.</li>
<li><strong>Tune autovacuum per-table</strong>: scale factor down to 0.01, cost delay to 0, cost limit way up, maintenance_work_mem to gigabytes.</li>
<li><strong>Drop unused indexes.</strong> Every one is a write tax and an extra full scan per vacuum.</li>
<li><strong>Partition</strong> if the table is large and the data has a natural key. This makes every future problem easier.</li>
<li><strong>For mass updates</strong>: batched keyset backfill, or partition rebuild-and-swap. Never one giant statement.</li>
</ol>
<p>The underlying idea is simple enough to hold in your head: <strong>PostgreSQL is a copy-on-write storage engine that trades write amplification for concurrency.</strong> Delete+insert opts into the maximum possible amplification — no HOT, full index maintenance, guaranteed dead tuples, guaranteed vacuum work. Understanding what happens on the 8 KB page is what turns “the database is slow” into a list of specific, fixable things.</p>
<p><em>If this was useful, the two commands worth internalizing are</em> <em>pageinspect&rsquo;s</em> <em>heap_page_items() and</em> <em>pgstattuple(). Point them at your hottest table. What you find will probably explain your CPU graph.</em></p>
<h3 id="references">References</h3>
<ul>
<li><a href="https://www.postgresql.org/docs/current/routine-vacuuming.html">PostgreSQL 18: Routine Vacuuming</a></li>
<li><a href="https://www.postgresql.org/docs/current/runtime-config-vacuum.html">PostgreSQL 18: Vacuuming configuration parameters</a></li>
<li><a href="https://www.postgresql.org/about/news/postgresql-18-released-3142/">PostgreSQL 18 Released</a></li>
<li><a href="https://techcommunity.microsoft.com/blog/adforpostgresql/postgresql-18-vacuuming-improvements-explained/4459484">PostgreSQL 18 Vacuuming Improvements Explained (Microsoft)</a></li>
<li><a href="https://www.dbi-services.com/blog/postgresql-18-introduce-autovacuum_vacuum_max_threshold/">autovacuum_vacuum_max_threshold in PostgreSQL 18 (dbi services)</a></li>
<li><a href="https://pganalyze.com/blog/5mins-postgres-17-faster-vacuum-adaptive-radix-trees">Faster VACUUM with Adaptive Radix Trees in PostgreSQL 17 (pganalyze)</a></li>
<li><a href="https://boringsql.com/posts/vacuum-at-the-page-level/">VACUUM at the Page Level (boringSQL)</a></li>
</ul>
]]></content:encoded></item><item><title>DateTime Handling in Java and PostgreSQL: A Practical Guide</title><link>https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/</link><pubDate>Sat, 15 Aug 2026 14:24:50 +0000</pubDate><guid>https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/</guid><description>&lt;p>&lt;em>Everything I wish someone had told me before I stored a meeting time in a&lt;/em> &lt;em>timestamp column.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-01.png">&lt;/p>
&lt;p>There is a specific kind of bug that only shows up in production, only in October, and only for users in one country. It gets closed as “not reproducible” twice before someone finally sits down with a coffee and reads the column definition.&lt;/p>
&lt;p>It is almost always a datetime bug.&lt;/p>
&lt;p>The frustrating part is that both Java and PostgreSQL have &lt;em>excellent&lt;/em> date/time support. java.time is one of the best-designed APIs in the JDK. PostgreSQL&amp;rsquo;s temporal types are precise and well documented. The bugs happen at the seam — in the assumptions each side makes about what the other means, and in the two or three words in the type names that mean the opposite of what you&amp;rsquo;d guess.&lt;/p></description><content:encoded><![CDATA[<p><em>Everything I wish someone had told me before I stored a meeting time in a</em> <em>timestamp column.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-01.png"></p>
<p>There is a specific kind of bug that only shows up in production, only in October, and only for users in one country. It gets closed as “not reproducible” twice before someone finally sits down with a coffee and reads the column definition.</p>
<p>It is almost always a datetime bug.</p>
<p>The frustrating part is that both Java and PostgreSQL have <em>excellent</em> date/time support. java.time is one of the best-designed APIs in the JDK. PostgreSQL&rsquo;s temporal types are precise and well documented. The bugs happen at the seam — in the assumptions each side makes about what the other means, and in the two or three words in the type names that mean the opposite of what you&rsquo;d guess.</p>
<p>This guide walks through both models, then spends real time on the part that actually causes outages: time zones.</p>
<p>Every code output and query result below was executed — Java 21 and PostgreSQL 16 — not written from memory.</p>
<h3 id="part-1-the-javaside">Part 1: The Java Side</h3>
<h4 id="ask-three-questions-get-onetype">Ask three questions, get one type</h4>
<p>Every java.time class is an answer to some combination of three questions:</p>
<ol>
<li>Do I know the <strong>date</strong>?</li>
<li>Do I know the <strong>time of day</strong>?</li>
<li>Do I know which <strong>point on the global timeline</strong> this is?</li>
</ol>
<p>That last one is the important one, and it’s the one people skip. Here’s the map:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-02.png"></p>
<p>The single most useful sentence in this article: <strong>LocalDateTime is not a point in time.</strong> 2026-08-15T12:00 is twelve o&rsquo;clock <em>somewhere</em>, and until you say where, it refers to a 26-hour-wide smear across the planet. If you have an Instant and you call .toLocalDateTime(), you have thrown away information and the compiler will not warn you.</p>
<h4 id="instant-is-the-boring-correctdefault">Instant is the boring, correct default</h4>
<pre tabindex="0"><code>Instant now = Instant.now();     // 2026-08-15T10:00:00.123456789Z
</code></pre><p>An Instant is nanoseconds since the epoch. No zone, no offset, no calendar. When it prints with a Z, that isn&rsquo;t a stored time zone — it&rsquo;s the ISO-8601 way of rendering an epoch value.</p>
<p>Anything that <em>happened</em> — a login, a payment, a row insert — is an Instant. Reach for something else only when you have a reason.</p>
<h4 id="zoneid-vs-zoneoffset-not-the-samething">ZoneId vs ZoneOffset: not the same thing</h4>
<p>This distinction matters more than any other in this article.</p>
<pre tabindex="0"><code>ZoneId.of(&#34;Europe/London&#34;)  // -&gt; java.time.ZoneRegion
ZoneId.of(&#34;+01:00&#34;)         // -&gt; java.time.ZoneOffset
</code></pre><p>A <strong>ZoneOffset</strong> is a fixed number of hours and minutes from UTC. +01:00. That&rsquo;s the whole thing. It&rsquo;s a fact about one moment.</p>
<p>A <strong>ZoneId</strong> (specifically a ZoneRegion, like Europe/London) is a <em>rulebook</em>. It knows that London was +00:00 in January, +01:00 in August, and it knows the exact instants at which it flips — going back decades and forward as far as legislators have declared.</p>
<p>ZoneOffset is a value. ZoneId is a function from instant to offset. Confusing them is the root cause of most scheduling bugs.</p>
<h4 id="offsetdatetime-vs-zoneddatetime">OffsetDateTime vs ZonedDateTime</h4>
<p>Both identify a real instant. The difference is what survives.</p>
<pre tabindex="0"><code>ZonedDateTime nyc = ZonedDateTime.of(2026, 7, 4, 12, 0, 0, 0,
                                     ZoneId.of(&#34;America/New_York&#34;));
nyc                    // 2026-07-04T12:00-04:00[America/New_York]
nyc.toOffsetDateTime() // 2026-07-04T12:00-04:00
nyc.toInstant()        // 2026-07-04T16:00:00Z
</code></pre><p>All three describe the same moment. But the ZonedDateTime still knows it&rsquo;s in New York, so if you add six months to it, it will correctly land on -05:00. The OffsetDateTime has been flattened — it remembers <em>that</em> the offset was -04:00, not <em>why</em>. Add six months and you get a wrong wall-clock time.</p>
<p>Rule of thumb: ZonedDateTime for calculating, OffsetDateTime for transporting, Instant for storing.</p>
<h4 id="duration-vsperiod">Duration vs Period</h4>
<pre tabindex="0"><code>LocalDate.of(2026, 1, 31).plusMonths(1)  // 2026-02-28
</code></pre><p>Duration is exact elapsed time — seconds and nanos. Period is calendar time — years, months, days. &ldquo;One month&rdquo; is not a fixed number of seconds, and plusMonths clamps rather than overflowing. Both behaviors are correct; mixing them up is not.</p>
<p>This distinction resurfaces the moment DST enters the picture, and we’ll come back to it.</p>
<h4 id="a-word-on-the-oldapi">A word on the old API</h4>
<p>java.util.Date is a mislabeled Instant (it has no time zone despite what toString() implies). Calendar is mutable and lenient. java.sql.Timestamp extends java.util.Date and adds nanoseconds while breaking the equals contract.</p>
<p>If you’re on a legacy codebase, convert at the boundary and never let them into your domain:</p>
<pre tabindex="0"><code>Instant i = oldDate.toInstant();
Date    d = Date.from(instant);
</code></pre><h3 id="part-2-the-postgresql-side">Part 2: The PostgreSQL Side</h3>
<h4 id="timestamptz-does-not-store-a-timezone">timestamptz does not store a time zone</h4>
<p>This is the single biggest source of confusion, and it comes straight from the type name. TIMESTAMP WITH TIME ZONE is an 8-byte value. So is TIMESTAMP WITHOUT TIME ZONE. There is no room in there for a zone, and PostgreSQL doesn&rsquo;t try:</p>
<pre tabindex="0"><code>SELECT pg_column_size(&#39;2026-08-15 12:00:00&#39;::timestamp)     AS ts_bytes,     
       --  8
       pg_column_size(&#39;2026-08-15 12:00:00+02&#39;::timestamptz) AS tstz_bytes,  
       --  8
       pg_column_size(&#39;12:00:00+02&#39;::timetz)                 AS timetz_bytes; 
       -- 12
</code></pre><p>What timestamptz actually does is <strong>convert to UTC on write and convert back on read</strong>. The offset you supplied on input is used and then discarded. From the <a href="https://www.postgresql.org/docs/current/datatype-datetime.html">official docs</a>:</p>
<blockquote>
<p><em>In either case, the value is stored internally as UTC, and the originally stated or assumed time zone is not retained.</em></p></blockquote>
<p>A better mental model: timestamptz is Instant. timestamp is LocalDateTime. The names lie; the semantics don&rsquo;t.</p>
<h4 id="the-demo-that-makes-itclick">The demo that makes it click</h4>
<p>Insert the same two literals into both column types, with the session zone set to London:</p>
<pre tabindex="0"><code>SET TimeZone = &#39;Europe/London&#39;;
CREATE TABLE t (id int, ts timestamp, tstz timestamptz);
INSERT INTO t VALUES (1, &#39;2026-08-15 12:00:00&#39;,    &#39;2026-08-15 12:00:00&#39;);
INSERT INTO t VALUES (2, &#39;2026-08-15 12:00:00+00&#39;, &#39;2026-08-15 12:00:00+00&#39;);
SELECT * FROM t;
</code></pre><pre tabindex="0"><code> id |         ts          |          tstz
----+---------------------+------------------------
  1 | 2026-08-15 12:00:00 | 2026-08-15 12:00:00+01
  2 | 2026-08-15 12:00:00 | 2026-08-15 13:00:00+01
</code></pre><p>Two things happened. Row 1’s tstz had no offset, so PostgreSQL <em>assumed</em> the session zone. Row 2&rsquo;s explicit +00 was honoured and re-rendered as 13:00+01. Meanwhile the ts column ignored the offset entirely and stored the digits.</p>
<p>Now reconnect from New York and run the identical SELECT:</p>
<pre tabindex="0"><code> id |         ts          |          tstz
----+---------------------+------------------------
  1 | 2026-08-15 12:00:00 | 2026-08-15 07:00:00-04
  2 | 2026-08-15 12:00:00 | 2026-08-15 08:00:00-04
</code></pre><p>The tstz column re-renders per client. The ts column is frozen. <strong>Neither row changed on disk.</strong></p>
<p>That last point deserves emphasis, because it’s where the panic starts: when a developer sees a different value than QA saw, the instinct is “the data is corrupted.” It isn’t. Two clients with different TimeZone settings are looking at the same UTC value through different windows.</p>
<h4 id="at-time-zone-runs-bothways">AT TIME ZONE runs both ways</h4>
<p>AT TIME ZONE is the one operator you need, and its behavior flips depending on the input type:</p>
<pre tabindex="0"><code>-- timestamptz -&gt; timestamp: &#34;what does the clock read there?&#34;
SELECT &#39;2026-08-15 12:00:00+00&#39;::timestamptz AT TIME ZONE &#39;Asia/Tokyo&#39;;
-- 2026-08-15 21:00:00   (type: timestamp without time zone)
</code></pre><pre tabindex="0"><code>-- timestamp -&gt; timestamptz: &#34;this wall clock reading WAS in Tokyo&#34;
SELECT &#39;2026-08-15 12:00:00&#39;::timestamp AT TIME ZONE &#39;Asia/Tokyo&#39;;
-- 2026-08-15 03:00:00+00   (type: timestamp with time zone)
</code></pre><p>One removes zone context, the other attaches it. Read it as “interpret this value in that zone.” The function form timezone(&lsquo;Asia/Tokyo&rsquo;, value) is identical.</p>
<h4 id="the-rest-of-thefamily">The rest of the family</h4>
<ul>
<li><strong>date</strong> — 4 bytes, no zone, no ambiguity. Great for things that are genuinely dates.</li>
<li><strong>time</strong> — time of day, no zone.</li>
<li><strong>timetz</strong> — 12 bytes, and <a href="https://www.postgresql.org/docs/current/datatype-datetime.html">PostgreSQL itself recommends against it</a>. A time of day with an offset but no date can&rsquo;t resolve DST, so it&rsquo;s not meaningfully anchored. It won&rsquo;t even accept a zone name:</li>
</ul>
<pre tabindex="0"><code>SELECT &#39;12:00:00 Europe/London&#39;::timetz; -- ERROR: invalid input syntax for type time with time zone
</code></pre><ul>
<li><strong>interval</strong> — 16 bytes storing months, days, and microseconds <em>separately</em>, which is exactly why it can be DST-aware.</li>
</ul>
<h4 id="now-vs-localtimestamp">now() vs localtimestamp</h4>
<pre tabindex="0"><code>SELECT pg_typeof(now()),            -- timestamp with time zone
       pg_typeof(current_timestamp), -- timestamp with time zone
       pg_typeof(localtimestamp),    -- timestamp without time zone
       pg_typeof(current_date);      -- date
</code></pre><p>now() and current_timestamp are timestamptz and are safe. localtimestamp and current_date are evaluated in the session zone, which means a DEFAULT current_date column produces different answers for different clients. Be deliberate.</p>
<h4 id="so-which-do-youpick">So which do you pick?</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-03.png"></p>
<p>The short version: <strong>use</strong> <strong>timestamptz for essentially everything.</strong> Reach for timestamp only when you genuinely mean a floating wall-clock time that must not shift — &ldquo;the store opens at 09:00 local, whatever local means at that branch&rdquo; — and store the zone ID beside it.</p>
<h3 id="part-3-time-zonesproperly">Part 3: Time Zones, Properly</h3>
<p>This is where the real complexity lives, so let’s slow down.</p>
<h4 id="three-things-called-timezone">Three things called “time zone”</h4>
<ol>
<li><strong>An offset</strong> — +01:00. A number.</li>
<li><strong>An IANA zone ID</strong> — Europe/London. A named set of historical and future rules. PostgreSQL ships ~499 of these in pg_timezone_names; Java reads the same tzdata.</li>
<li><strong>An abbreviation</strong> — EST, BST, IST. Ambiguous, unversioned, and a trap.</li>
</ol>
<p>That third one is genuinely dangerous:</p>
<pre tabindex="0"><code>SELECT &#39;2026-08-15 12:00:00 EST&#39;::timestamptz,               -- 2026-08-15 17:00:00+00
       &#39;2026-08-15 12:00:00 America/New_York&#39;::timestamptz;  -- 2026-08-15 16:00:00+00
</code></pre><p>One hour apart. EST is a <em>fixed</em> -05:00 in PostgreSQL — it does not mean &ldquo;Eastern time,&rdquo; it means &ldquo;Eastern Standard Time, always.&rdquo; In August, New York is on -04:00. If you accept zone abbreviations from user input, you have already shipped a bug. (IST is worse — it can mean India, Ireland, or Israel depending on who you ask.)</p>
<p><strong>Always use full IANA names.</strong></p>
<p>Offsets aren’t zones either. Nepal is +05:45; the Chatham Islands are +12:45:</p>
<pre tabindex="0"><code>ZonedDateTime.of(2026,8,15,12,0,0,0, ZoneId.of(&#34;Asia/Kathmandu&#34;))
// 2026-08-15T12:00+05:45[Asia/Kathmandu]
</code></pre><p>If your API contract says “offset in whole hours,” it’s wrong.</p>
<h3 id="where-postgresqls-session-zone-comesfrom">Where PostgreSQL’s session zone comes from</h3>
<p>The TimeZone GUC is a <strong>per-connection setting</strong>. Its value is decided by, in order: an explicit SET TimeZone, the connection&rsquo;s startup parameters, the postgresql.conf value, and finally the server&rsquo;s OS zone.</p>
<p>That last fallback is why the same query returns different results on a developer laptop and in CI. And with connection pooling, SET TimeZone on a borrowed connection leaks to whoever gets it next.</p>
<p>Pin it. Set timezone = &lsquo;UTC&rsquo; in postgresql.conf, and add it to the JDBC URL so it survives pool reuse:</p>
<pre tabindex="0"><code>jdbc:postgresql://host:5432/db?options=-c%20TimeZone%3DUTC
</code></pre><p>Then make the client responsible for rendering. Formatting a timestamp for a human is a presentation concern, not a database one.</p>
<h4 id="the-spring-forward-gap">The spring-forward gap</h4>
<p>On 29 March 2026, London jumps from 01:00 to 02:00. The local times between them <strong>do not exist</strong>.</p>
<p>Ask Java for one anyway:</p>
<pre tabindex="0"><code>LocalDateTime gap = LocalDateTime.of(2026, 3, 29, 1, 30);
gap.atZone(ZoneId.of(&#34;Europe/London&#34;));
// 2026-03-29T02:30+01:00[Europe/London]
ZoneId.of(&#34;Europe/London&#34;).getRules().getTransition(gap);
// Transition[Gap at 2026-03-29T01:00Z to +01:00]
</code></pre><p>Java doesn’t throw. It silently pushes the value forward by the length of the gap. PostgreSQL does the same thing:</p>
<pre tabindex="0"><code>SET TimeZone=&#39;Europe/London&#39;;
SELECT &#39;2026-03-29 01:30:00&#39;::timestamptz;
-- 2026-03-29 02:30:00+01
</code></pre><p>Both are “correct” — but neither told you. If a user sets a 01:30 alarm on that date, you should be validating with getTransition() and telling them, not letting the platform quietly reschedule.</p>
<h4 id="the-fall-back-overlapwhere-java-and-postgresql-disagree">The fall-back overlap — where Java and PostgreSQL disagree</h4>
<p>On 25 October 2026, London goes from 02:00 back to 01:00. Local times between 01:00 and 02:00 happen <strong>twice</strong>, one hour apart.</p>
<p>Java’s rules expose both:</p>
<pre tabindex="0"><code>ZoneId london = ZoneId.of(&#34;Europe/London&#34;);
LocalDateTime overlap = LocalDateTime.of(2026, 10, 25, 1, 30);
london.getRules().getValidOffsets(overlap);
// [+01:00, Z]
ZonedDateTime earlier = overlap.atZone(london);
// 2026-10-25T01:30+01:00[Europe/London]  instant = 2026-10-25T00:30:00Z
ZonedDateTime later = earlier.withLaterOffsetAtOverlap();
// 2026-10-25T01:30Z[Europe/London]       instant = 2026-10-25T01:30:00Z
</code></pre><p>Java’s default for an ambiguous local time is the <strong>earlier</strong> offset — 00:30Z.</p>
<p>Now ask PostgreSQL the same question:</p>
<pre tabindex="0"><code>SELECT &#39;2026-10-25 01:30:00&#39;::timestamp AT TIME ZONE &#39;Europe/London&#39;;
-- 2026-10-25 01:30:00+00
</code></pre><p>PostgreSQL picked 01:30Z — the <strong>later</strong> one.</p>
<p>Same input, same tzdata, one hour apart. If your application resolves an ambiguous local time in Java and your reporting job resolves it in SQL, they will disagree for one hour, twice a year, in every DST zone.</p>
<p>There is no clever fix. The lesson is: <strong>never let a</strong> <strong>LocalDateTime cross the boundary and get resolved on the other side.</strong> Resolve to an Instant in exactly one place, with explicit overlap handling, and send the instant.</p>
<h4 id="dst-arithmetic-plusdays-is-not-plushours24">DST arithmetic: plusDays is not plusHours(24)</h4>
<pre tabindex="0"><code>ZonedDateTime start = ZonedDateTime.of(2026, 3, 28, 12, 0, 0, 0, london);
// 2026-03-28T12:00Z[Europe/London]
start.plusDays(1);                 // 2026-03-29T12:00+01:00  (23 real hours)
start.plus(Duration.ofDays(1));    // 2026-03-29T13:00+01:00  (24 real hours)
start.plusHours(24);               // 2026-03-29T13:00+01:00
</code></pre><p>plusDays preserves the wall clock. Duration preserves elapsed time. PostgreSQL draws the same line, using the same value:</p>
<pre tabindex="0"><code>SET TimeZone=&#39;Europe/London&#39;;
SELECT &#39;2026-03-28 12:00:00+00&#39;::timestamptz + interval &#39;1 day&#39;,   -- 2026-03-29 12:00:00+01
       &#39;2026-03-28 12:00:00+00&#39;::timestamptz + interval &#39;24 hours&#39;; -- 2026-03-29 13:00:00+01
</code></pre><p>This is one place the two platforms agree beautifully — interval &lsquo;1 day&rsquo; and plusDays(1) mean the same thing, and interval &lsquo;24 hours&rsquo; and Duration.ofDays(1) mean the same thing. &ldquo;Every day at 9am&rdquo; is plusDays / interval &lsquo;1 day&rsquo;. &ldquo;Every 24 hours&rdquo; is Duration / interval &lsquo;24 hours&rsquo;. Pick the one the product actually means.</p>
<h4 id="future-events-an-instant-is-notenough">Future events: an instant is not enough</h4>
<pre tabindex="0"><code>ZoneId ny = ZoneId.of(&#34;America/New_York&#34;);
ZonedDateTime.of(2027, 1, 15, 9, 0, 0, 0, ny).withZoneSameInstant(UTC); // 2027-01-15T14:00Z
ZonedDateTime.of(2027, 7, 15, 9, 0, 0, 0, ny).withZoneSameInstant(UTC); // 2027-07-15T13:00Z
</code></pre><p>“9am in New York” is a different UTC instant depending on the date. So if a user schedules a recurring 9am standup and a government later changes DST rules — which happens several times a year somewhere — the stored instant becomes wrong.</p>
<p>For <strong>past events</strong>, store the instant. That’s history; it can’t change.</p>
<p>For <strong>future user-scheduled events</strong>, store three things:</p>
<pre tabindex="0"><code>CREATE TABLE meetings (
  id            bigserial PRIMARY KEY,
  local_start   timestamp   NOT NULL,  -- 2027-01-15 09:00
  zone_id       text        NOT NULL,  -- &#39;America/New_York&#39;
  computed_utc  timestamptz NOT NULL   -- cache; recompute on tzdata update
);
</code></pre><p>The first two are the truth. The third is a derived index for querying, refreshed when tzdata changes.</p>
<h4 id="tzdata-is-a-movingtarget">tzdata is a moving target</h4>
<p>Java bundles a copy of the IANA database (2025b on the JDK I ran this on); PostgreSQL bundles its own; your OS has a third. They drift. When Java and PostgreSQL are on different tzdata versions, they will resolve recently-changed zones differently.</p>
<p>Check what you have:</p>
<pre tabindex="0"><code>ZoneRulesProvider.getVersions(&#34;Europe/London&#34;).lastKey();  // 2025b
</code></pre><pre tabindex="0"><code>SELECT name, utc_offset, is_dst FROM pg_timezone_names WHERE name = &#39;Europe/London&#39;;
</code></pre><p>Pin tzdata in your base images, and treat a tzdata bump as a deployment that requires recomputing cached future instants.</p>
<h3 id="part-4-wiring-java-to-postgresql">Part 4: Wiring Java to PostgreSQL</h3>
<h4 id="the-mappingtable">The mapping table</h4>
<p>Per the <a href="https://jdbc.postgresql.org/documentation/query/">pgJDBC documentation</a>:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-04.png"></p>
<p>Note the omission, in the driver’s own words:</p>
<blockquote>
<p><em>ZonedDateTime, Instant and OffsetTime / TIME WITH TIME ZONE are not supported.</em></p></blockquote>
<p>So don’t hand the driver an Instant and hope — depending on the driver version you&rsquo;ll either get a Can&rsquo;t infer the SQL type to use for an instance of java.time.Instant exception or undocumented behavior. Convert explicitly at the boundary instead:</p>
<pre tabindex="0"><code>// write
ps.setObject(1, instant.atOffset(ZoneOffset.UTC));
// read
Instant i = rs.getObject(&#34;occurred_at&#34;, OffsetDateTime.class).toInstant();
</code></pre><p>Note that reading a timestamptz gives you an OffsetDateTime normalized to UTC — the driver&rsquo;s offset is always Z, because that&rsquo;s what the backend holds. The offset carries no information. Convert straight to Instant and don&rsquo;t be tempted to read meaning into it.</p>
<h4 id="avoid-settimestamp--gettimestamp">Avoid setTimestamp / getTimestamp</h4>
<p>The java.sql.Timestamp overloads convert using the <strong>JVM default time zone</strong> unless you pass an explicit Calendar. That makes your persistence layer&rsquo;s correctness depend on the container&rsquo;s TZ environment variable. Use the java.time setObject/getObject methods instead — they&rsquo;re unambiguous.</p>
<p>If you must keep the JVM zone from mattering at all, set it explicitly at startup:</p>
<pre tabindex="0"><code>-Duser.timezone=UTC
</code></pre><p>Belt and braces, not a substitute for correct types.</p>
<h4 id="precision-java-has-nanos-postgresql-hasmicros">Precision: Java has nanos, PostgreSQL has micros</h4>
<pre tabindex="0"><code>Instant.parse(&#34;2026-08-15T10:00:00.123456789Z&#34;)           // .123456789
    .truncatedTo(ChronoUnit.MICROS)                        // .123456
</code></pre><p>PostgreSQL stores microseconds. Those last three digits are gone on the round trip, and PostgreSQL may <strong>round</strong> rather than truncate:</p>
<pre tabindex="0"><code>SELECT &#39;2026-08-15 10:00:00.123456789&#39;::timestamptz;  -- 10:00:00.123457
</code></pre><p>If you have an equality test or an optimistic-lock check comparing an in-memory Instant to one that&rsquo;s been through the database, truncate to micros <em>before</em> you persist so both sides agree.</p>
<h4 id="dont-wrap-the-column-in-afunction">Don’t wrap the column in a function</h4>
<p>Because timestamptz is stored as UTC, &ldquo;give me everything that happened on 1 Feb in New York&rdquo; tempts you into this:</p>
<pre tabindex="0"><code>EXPLAIN SELECT count(*) FROM events
WHERE (occurred_at AT TIME ZONE &#39;America/New_York&#39;)::date = DATE &#39;2026-02-01&#39;;
--  Seq Scan on events
</code></pre><p>Index gone. Compute the boundary instants instead and do a half-open range scan:</p>
<pre tabindex="0"><code>SELECT (DATE &#39;2026-02-01&#39; + time &#39;00:00&#39;) AT TIME ZONE &#39;America/New_York&#39;,  -- 2026-02-01 05:00+00
       (DATE &#39;2026-02-02&#39; + time &#39;00:00&#39;) AT TIME ZONE &#39;America/New_York&#39;;  -- 2026-02-02 05:00+00
EXPLAIN SELECT count(*) FROM events
WHERE occurred_at &gt;= &#39;2026-02-01 05:00:00+00&#39;
  AND occurred_at &lt;  &#39;2026-02-02 05:00:00+00&#39;;
--  Index Only Scan using events_occurred_at_idx
</code></pre><p>Note that this “day” is 24 hours only because February has no DST transition. On a transition day it’s 23 or 25 — which the boundary-instant approach handles correctly and a naive + interval &lsquo;24 hours&rsquo; does not. Half-open ranges (&gt;= and &lt;) also dodge the microsecond-boundary problem that BETWEEN creates.</p>
<h3 id="part-5-jpa-and-hibernate">Part 5: JPA and Hibernate</h3>
<p>Hibernate 6 made zoned types a first-class concern via TimeZoneStorageType:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/datetime-handling-in-java-and-postgresql-a-practical-guide/image-05.png"></p>
<p>For PostgreSQL, the practical consequence is settled by the database, not the ORM: timestamptz discards the offset, so under DEFAULT your OffsetDateTime round-trips as UTC. Which is fine — as long as you expected it.</p>
<p>Sensible defaults:</p>
<pre tabindex="0"><code>spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          time_zone: UTC
        timezone:
          default_storage: NORMALIZE_UTC
</code></pre><p>And in the entity, be explicit about intent:</p>
<pre tabindex="0"><code>@Entity
public class Booking {
    
    // A moment. Maps to timestamptz.
    @Column(nullable = false)
    private Instant createdAt;
    
    // A date. Not a moment.
    @Column(nullable = false)
    private LocalDate serviceDate;
    
    // A wall-clock time that must not shift. Maps to timestamp.
    @Column(nullable = false)
    private LocalDateTime localStart;
    
    @Column(nullable = false)
    private String zoneId;   // &#39;Europe/London&#39;
    
    public ZonedDateTime start() {
        return localStart.atZone(ZoneId.of(zoneId));
    }
}
</code></pre><p>If you genuinely need the user’s original offset preserved, @TimeZoneStorage(TimeZoneStorageType.COLUMN) with @TimeZoneColumn writes it to a companion column. Use it when the offset is <em>data</em> — an audit trail of what the user&rsquo;s device claimed — not as a default.</p>
<h3 id="part-6-testing-thisstuff">Part 6: Testing This Stuff</h3>
<p>Datetime bugs are only cheap if you catch them before the clocks change. Three habits do most of the work.</p>
<h4 id="1-inject-aclock">1. Inject a Clock</h4>
<p>Instant.now() scattered through business logic is untestable. Inject java.time.Clock and you can move time at will:</p>
<pre tabindex="0"><code>@Bean Clock clock() { return Clock.systemUTC(); }
// in a test
Clock fixed = Clock.fixed(Instant.parse(&#34;2026-10-25T00:30:00Z&#34;), ZoneOffset.UTC);
</code></pre><h4 id="2-run-ci-in-a-hostile-timezone">2. Run CI in a hostile time zone</h4>
<p>If your JVM, your database, and your CI runner are all UTC, every zone bug hides until production. Force one of them somewhere awkward:</p>
<pre tabindex="0"><code>-Duser.timezone=Pacific/Chatham
</code></pre><p>+12:45 and it observes DST. If your test suite survives Chatham, it will survive most things. Do the same for the PostgreSQL container:</p>
<pre tabindex="0"><code>@Container
static PostgreSQLContainer&lt;?&gt; pg = new PostgreSQLContainer&lt;&gt;(&#34;postgres:16&#34;)
        .withEnv(&#34;TZ&#34;, &#34;Asia/Kathmandu&#34;)
        .withUrlParam(&#34;options&#34;, &#34;-c TimeZone=Asia/Kathmandu&#34;);
</code></pre><p>The point isn’t that production runs there — it’s that a test which only passes in UTC is a test that has proven nothing about zone handling.</p>
<h4 id="3-write-the-four-transition-tests">3. Write the four transition tests</h4>
<p>For any code that converts between local and instant, pin these explicitly:</p>
<pre tabindex="0"><code>static final ZoneId LONDON = ZoneId.of(&#34;Europe/London&#34;);
@Test void gapTimesAreRejectedNotSilentlyShifted() {
    LocalDateTime gap = LocalDateTime.of(2026, 3, 29, 1, 30);
    assertThat(LONDON.getRules().getValidOffsets(gap)).isEmpty();
    assertThatThrownBy(() -&gt; scheduler.schedule(gap, LONDON))
        .isInstanceOf(NonExistentLocalTimeException.class);
}
@Test void overlapTimesResolveToTheDocumentedChoice() {
    LocalDateTime overlap = LocalDateTime.of(2026, 10, 25, 1, 30);
    assertThat(LONDON.getRules().getValidOffsets(overlap)).hasSize(2);
    assertThat(scheduler.resolve(overlap, LONDON))
        .isEqualTo(Instant.parse(&#34;2026-10-25T00:30:00Z&#34;)); // earlier, by policy
}
@Test void dailyRecurrenceKeepsWallClockAcrossDst() {
    ZonedDateTime start = ZonedDateTime.of(2026, 3, 28, 9, 0, 0, 0, LONDON);
    assertThat(recurrence.next(start).toLocalTime()).isEqualTo(LocalTime.of(9, 0));
}
@Test void instantSurvivesTheDatabaseRoundTrip() {
    Instant original = Instant.now().truncatedTo(ChronoUnit.MICROS);
    repository.save(new Event(original));
    assertThat(repository.findLatest().occurredAt()).isEqualTo(original);
}
</code></pre><p>The fourth one catches more real bugs than you’d expect — it’s the one that fails when someone quietly changes a column from timestamptz to timestamp.</p>
<h3 id="the-shortversion">The Short Version</h3>
<p>If you skim one section, make it this one.</p>
<p><strong>Schema</strong></p>
<ul>
<li>timestamptz by default. It&rsquo;s the same 8 bytes and it&rsquo;s unambiguous.</li>
<li>timestamp only for wall-clock times that must not shift — and store the zone ID beside it.</li>
<li>date for dates. timetz for nothing.</li>
<li>Pin timezone = &lsquo;UTC&rsquo; in postgresql.conf and in the JDBC URL.</li>
</ul>
<p><strong>Java</strong></p>
<ul>
<li>Instant for things that happened. LocalDate for dates. ZonedDateTime for scheduling. OffsetDateTime at the JDBC boundary.</li>
<li>LocalDateTime is not a point in time. Treat every use as a question to answer, not an answer.</li>
<li>Never accept zone abbreviations. Full IANA names only.</li>
<li>plusDays ≠ plus(Duration.ofDays(1)). Know which one your product means.</li>
</ul>
<p><strong>The seam</strong></p>
<ul>
<li>pgJDBC speaks LocalDate/LocalTime/LocalDateTime/OffsetDateTime. Convert Instant explicitly.</li>
<li>Avoid setTimestamp/getTimestamp — they use the JVM default zone.</li>
<li>Truncate to microseconds before persisting.</li>
<li>Don’t wrap indexed timestamp columns in AT TIME ZONE; compute boundary instants instead.</li>
</ul>
<p><strong>And the one that bites hardest</strong></p>
<p>Java resolves an ambiguous local time to the <em>earlier</em> offset. PostgreSQL picks the <em>later</em> one. Resolve ambiguity in exactly one place, deliberately, and pass instants across the boundary — never local times.</p>
<p>Store the instant, render the local time, and keep the zone rules in exactly one layer. Most datetime bugs are just that rule, broken somewhere.</p>
<p><em>All examples verified on OpenJDK 21.0.10 (tzdata 2025b) and PostgreSQL 16.13.</em></p>
<p><strong>Sources</strong></p>
<ul>
<li><a href="https://www.postgresql.org/docs/current/datatype-datetime.html">PostgreSQL: Date/Time Types</a></li>
<li><a href="https://jdbc.postgresql.org/documentation/query/">pgJDBC: Using Java 8 Date and Time classes</a></li>
<li><a href="https://docs.hibernate.org/orm/6.4/javadocs/org/hibernate/annotations/TimeZoneStorageType.html">Hibernate</a> <a href="https://docs.hibernate.org/orm/6.4/javadocs/org/hibernate/annotations/TimeZoneStorageType.html">TimeZoneStorageType Javadoc</a></li>
<li><a href="https://docs.hibernate.org/orm/6.2/migration-guide/">Hibernate 6 migration guide</a></li>
<li><a href="https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/">Thorben Janssen: TimezoneStorageType</a></li>
</ul>
]]></content:encoded></item><item><title>Debugging Postgres: The System Tables That Actually Tell You What’s Wrong</title><link>https://sukachvitally.github.io/posts/debugging-postgres-the-system-tables-that-actually-tell-you-whats-wrong/</link><pubDate>Sun, 02 Aug 2026 08:37:40 +0000</pubDate><guid>https://sukachvitally.github.io/posts/debugging-postgres-the-system-tables-that-actually-tell-you-whats-wrong/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/debugging-postgres-the-system-tables-that-actually-tell-you-whats-wrong/image-01.png">&lt;/p>
&lt;p>When a migration hangs, a query slows to a crawl, or a deploy just sits there, most people reach for the same first move: kill it and try again. That works about as often as it doesn’t. Postgres already knows what’s wrong — it’s just keeping the answer in a handful of system catalogs and stats views that almost nobody looks at until things are already on fire.&lt;/p>
&lt;p>This is a walkthrough of the tables I actually check, in the order I check them, when something breaks — with a running example: a migration that adds a NOT NULL column and backfills it on a 20-million-row orders table, which has been &amp;ldquo;running&amp;rdquo; for 25 minutes with no sign of finishing.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/debugging-postgres-the-system-tables-that-actually-tell-you-whats-wrong/image-01.png"></p>
<p>When a migration hangs, a query slows to a crawl, or a deploy just sits there, most people reach for the same first move: kill it and try again. That works about as often as it doesn’t. Postgres already knows what’s wrong — it’s just keeping the answer in a handful of system catalogs and stats views that almost nobody looks at until things are already on fire.</p>
<p>This is a walkthrough of the tables I actually check, in the order I check them, when something breaks — with a running example: a migration that adds a NOT NULL column and backfills it on a 20-million-row orders table, which has been &ldquo;running&rdquo; for 25 minutes with no sign of finishing.</p>
<h3 id="start-with-is-it-evenrunning">Start with: is it even running?</h3>
<p>Before touching statistics or query plans, find out if the process is alive, blocked, or just slow.</p>
<pre tabindex="0"><code>SELECT pid, state, wait_event_type, wait_event, query, query_start
FROM pg_stat_activity
WHERE state != &#39;idle&#39;;
</code></pre><p>pg_stat_activity is a live snapshot of every connection to the database. The columns that matter most here are wait_event_type and wait_event. If a process shows wait_event_type = &lsquo;Lock&rsquo;, it isn&rsquo;t broken — it&rsquo;s queued behind something else. If it shows Client or IO, that&rsquo;s a different problem entirely (waiting on the client, or waiting on disk).</p>
<p>For the orders migration, this comes back with:</p>
<pre tabindex="0"><code>pid  | state  | wait_event_type | wait_event | query
-----+--------+-----------------+------------+----------------------------------
8842 | active | Lock            | relation   | ALTER TABLE orders ADD COLUMN ...
</code></pre><p>wait_event_type = Lock tells us immediately: the migration isn&rsquo;t slow, it&rsquo;s stuck behind something else that holds a conflicting lock on orders.</p>
<p>If you see a lock wait, the next stop is obvious:</p>
<pre tabindex="0"><code>SELECT blocked_locks.pid AS blocked_pid,
       blocking_locks.pid AS blocking_pid,
       blocked_activity.query AS blocked_query,
       blocking_activity.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_locks blocking_locks
  ON blocking_locks.locktype = blocked_locks.locktype
  AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
  AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
  AND blocking_locks.pid != blocked_locks.pid
JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
</code></pre><p>This join is the single most useful query in Postgres debugging. It tells you exactly which session is blocking which, and what each of them is running. Most “stuck migration” incidents end here: some other transaction — often a long-running read, an ORM that forgot to close a connection, or a previous deploy that never finished — is sitting on a lock the migration needs.</p>
<p>Running it for the orders case turns up the problem:</p>
<pre tabindex="0"><code>blocked_pid | blocking_pid | blocked_query                   | blocking_query
------------+--------------+---------------------------------+---------------------------------------------
 8842       | 6110         | ALTER TABLE orders ADD COLUMN.. | SELECT * FROM orders WHERE ... (idle in txn)
</code></pre><p>PID 6110 is an analytics job that opened a transaction, ran a read, and never committed — “idle in transaction” — leaving an ACCESS SHARE lock on orders that blocks the migration&rsquo;s ACCESS EXCLUSIVE lock. The fix here isn&rsquo;t in the migration at all: it&rsquo;s killing or fixing PID 6110 (SELECT pg_terminate_backend(6110); if you&rsquo;re sure it&rsquo;s safe to end it).</p>
<h3 id="if-its-a-long-running-operation-check-its-actualprogress">If it’s a long-running operation, check its actual progress</h3>
<p>Postgres exposes live progress for the operations that most commonly make migrations feel frozen:</p>
<pre tabindex="0"><code>SELECT * FROM pg_stat_progress_create_index;
SELECT * FROM pg_stat_progress_vacuum;
SELECT * FROM pg_stat_progress_analyze;
</code></pre><p>If you’re running CREATE INDEX CONCURRENTLY as part of a migration, pg_stat_progress_create_index shows you the current phase (building index, index validation, etc.) and how many tuples it has scanned versus the total. This turns &ldquo;is this hung or just big?&rdquo; from a guess into a fact.</p>
<p>Say the next step of the migration adds an index on the new column instead of blocking on a lock — the progress view might show:</p>
<pre tabindex="0"><code>pid  | phase                         | tuples_total | tuples_done
-----+-------------------------------+--------------+-------------
9021 | building index: scanning table| 20450112     | 14300556
</code></pre><p>That’s roughly 70% through a single scan phase, moving steadily — not stuck, just genuinely working through 20 million rows. Compare that against a snapshot taken a minute later: if tuples_done hasn&rsquo;t moved, now you know it actually is stalled, and it&rsquo;s worth going back to pg_locks.</p>
<h3 id="if-nothings-blocked-suspect-the-queryplan">If nothing’s blocked, suspect the query plan</h3>
<p>Not every broken migration is a lock problem. Sometimes the migration runs, but a backfill step or an added index makes the planner choose a catastrophically bad plan. That’s a statistics problem, and it lives in two places.</p>
<p><strong>pg_stat_user_tables</strong> tells you whether Postgres has been keeping its stats up to date on the table in question:</p>
<pre tabindex="0"><code>SELECT relname, n_live_tup, n_dead_tup, n_mod_since_analyze,
       last_vacuum, last_autovacuum, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = &#39;your_table&#39;;
</code></pre><p>If n_mod_since_analyze is large relative to n_live_tup, or last_autoanalyze is suspiciously old, the planner is working off outdated information. This happens constantly during migrations, because a migration that inserts, updates, or deletes a large chunk of a table changes its shape faster than autovacuum&rsquo;s default thresholds react to.</p>
<p>For orders, after the backfill step runs:</p>
<pre tabindex="0"><code>relname | n_live_tup | n_dead_tup | n_mod_since_analyze | last_autoanalyze
--------+------------+------------+---------------------+-------------------
orders  | 20450112   | 3800000    | 18900000            | 2026-07-28 03:14
</code></pre><p>18.9 million modified rows since the last analyze, against 20 million live rows — nearly the entire table has changed and autovacuum hasn’t caught up yet (last_autoanalyze is five days old). Any query the migration runs after this point is planning against numbers that are effectively meaningless.</p>
<p><strong>pg_stats</strong> (the readable view over the raw pg_statistic catalog) shows what the planner actually believes about each column:</p>
<pre tabindex="0"><code>SELECT attname, n_distinct, correlation, null_frac, most_common_vals
FROM pg_stats
WHERE tablename = &#39;your_table&#39;;
</code></pre><p>n_distinct and correlation drive index-vs-sequential-scan decisions. If a migration adds a new index and the planner still refuses to use it, stale stats here are the usual suspect.</p>
<p>On the new orders.status column right after the backfill, before an ANALYZE:</p>
<pre tabindex="0"><code> attname | n_distinct | correlation | null_frac
---------+------------+-------------+-----------
 status  | 1          | 1           | 1.0
</code></pre><p>The planner still believes every row is null (null_frac = 1.0) because that was true before the backfill ran. Any query filtering on status will get a wildly wrong row estimate and likely pick a sequential scan or a bad join order. The fix is almost always to run ANALYZE your_table; — either before the migration, or as an explicit step inside it, rather than waiting for autovacuum to get around to it. After ANALYZE orders;, the same query returns something like n_distinct = 4, null_frac = 0.0 — matching reality, and the planner&rsquo;s decisions follow accordingly.</p>
<p><strong>pg_class</strong> holds the planner&rsquo;s cached size estimates directly:</p>
<pre tabindex="0"><code>SELECT relname, reltuples, relpages
FROM pg_class
WHERE relname = &#39;your_table&#39;;
</code></pre><p>reltuples is an estimate, not a live count — it&rsquo;s only refreshed by ANALYZE or VACUUM. If this number is wildly off from the real row count, every cost estimate downstream is wrong too. For orders, reltuples might read 20450112 while a SELECT count(*) returns closer to 24000000 — a four-million-row gap that skews every cost estimate the planner builds on top of it.</p>
<h3 id="if-you-need-the-exact-query-and-its-cost-not-just-thetable">If you need the exact query and its cost, not just the table</h3>
<p>pg_stat_statements (an extension, so it needs to be enabled) aggregates every distinct query the server has run, with real timing:</p>
<pre tabindex="0"><code>SELECT query, calls, total_exec_time, mean_exec_time, rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
</code></pre><p>This is the difference between “the migration is slow” and “this specific UPDATE inside the migration is slow, and it’s been called 40,000 times.” For the orders backfill, this might show:</p>
<pre tabindex="0"><code>query                                        | calls | mean_exec_time | rows
---------------------------------------------+-------+----------------+--------
 UPDATE orders SET status = $1 WHERE id = $2 | 40000 | 812.4 ms       | 40000
</code></pre><p>40,000 single-row updates averaging 800ms each explains a lot on its own — that’s batching the backfill row-by-row instead of in bulk, and it would still be slow even with perfect statistics. If it’s enabled in your environment, it’s usually the fastest way to go from vague suspicion to a concrete line of SQL to fix — in this case, rewriting the backfill as a single set-based UPDATE &hellip; WHERE status IS NULL in batches, rather than one row at a time.</p>
<h3 id="a-rough-order-of-operations">A rough order of operations</h3>
<p>Put together, a sensible debugging sequence looks like this: check pg_stat_activity to see if the process is alive and what it&rsquo;s waiting on; if it&rsquo;s waiting on a lock, join into pg_locks to find the blocker; if it&rsquo;s a long index build or vacuum, check the matching pg_stat_progress_* view for real progress; if nothing is blocked but it&rsquo;s still slow, check pg_stat_user_tables and pg_stats for stale statistics, and pg_class for stale row estimates; and if pg_stat_statements is available, use it to find the exact query eating the time.</p>
<p>None of these tables are exotic. They ship with every Postgres install (aside from pg_stat_statements, which just needs CREATE EXTENSION). The only real skill here is knowing which one answers which question — and reaching for pg_stat_activity and pg_locks before assuming it&rsquo;s a statistics problem, since lock contention is a far more common cause of a &ldquo;broken&rdquo; migration than a bad query plan.</p>
]]></content:encoded></item><item><title>From Source to Running JVM: How Gradle Builds Your Java Application</title><link>https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/</link><pubDate>Sun, 19 Jul 2026 11:14:16 +0000</pubDate><guid>https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/</guid><description>&lt;p>Build phases, task graphs, dependency resolution, and what actually happens when the JVM opens your JAR&lt;/p>
&lt;p>You type gradle build, wait a few seconds, and out comes a JAR. Most Java developers use this pipeline every day without ever looking inside it. But when a build gets slow, a dependency conflict appears, or a NoClassDefFoundError shows up in production at 2 AM, understanding the machinery underneath is the difference between guessing and fixing.&lt;/p></description><content:encoded><![CDATA[<p>Build phases, task graphs, dependency resolution, and what actually happens when the JVM opens your JAR</p>
<p>You type gradle build, wait a few seconds, and out comes a JAR. Most Java developers use this pipeline every day without ever looking inside it. But when a build gets slow, a dependency conflict appears, or a NoClassDefFoundError shows up in production at 2 AM, understanding the machinery underneath is the difference between guessing and fixing.</p>
<p>This article walks the full journey: Gradle’s three build phases, the Java plugin’s task graph, how dependency resolution actually works stage by stage, what a JAR file really is, and how the JVM reads and executes it.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-01.png"></p>
<h3 id="part-1-the-three-phases-every-gradle-build-goesthrough">Part 1: The Three Phases Every Gradle Build Goes Through</h3>
<p>Before a single line of your code is compiled, Gradle itself runs through a fixed lifecycle of three phases. This is true for every build, every plugin, every project.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-02.png"></p>
<h4 id="phase-1initialization">Phase 1 — Initialization</h4>
<p>Gradle reads settings.gradle(.kts) and answers one question: <em>which projects are part of this build?</em> For a single-module app that&rsquo;s trivial; for a multi-module monorepo this is where include(&quot;:core&quot;, &ldquo;:api&rdquo;, &ldquo;:app&rdquo;) gets evaluated and a Project object is created for each module.</p>
<h4 id="phase-2configuration">Phase 2 — Configuration</h4>
<p>Now every build.gradle(.kts) script executes top to bottom. This is the phase people misunderstand most. Your build script is <em>code that runs</em> — and it runs here, on every invocation, regardless of which task you asked for. The output of this phase is the <strong>task graph</strong>: a directed acyclic graph (DAG) of all tasks and their dependencies. Tasks are registered and wired together, but none of their actions execute yet.</p>
<blockquote>
<p><em>A</em> <em>println at the top level of build.gradle prints during Configuration — even if you run</em> <em>gradle help. Task actions run later. This trips up nearly everyone once.</em></p></blockquote>
<h4 id="phase-3execution">Phase 3 — Execution</h4>
<p>Gradle takes the task you requested, computes the minimal subgraph of tasks needed to produce it, and executes them in dependency order. Tasks whose inputs and outputs haven’t changed since the last run are marked UP-TO-DATE and skipped entirely — this is Gradle&rsquo;s incremental build engine, and it&rsquo;s why the second build is always faster than the first.</p>
<h3 id="part-2-the-java-plugin-taskgraph">Part 2: The Java Plugin Task Graph</h3>
<p>Applying the java plugin registers a standard chain of tasks. When you run gradle build, this is the graph that gets executed:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-03.png"></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-04.png"></p>
<p>The practical shortcuts worth memorizing:</p>
<pre tabindex="0"><code># fastest feedback: compile only
gradle compileJava
# package without running tests
gradle jar
# full build, skip test execution
gradle build -x test
# nuke build/ and start clean
gradle clean build
</code></pre><h3 id="part-3-how-gradle-dependency-resolution-actuallyworks">Part 3: How Gradle Dependency Resolution Actually Works</h3>
<p>This is the part of Gradle most developers treat as magic. You write three lines in a dependencies block, and somehow the right JARs appear on your classpath. Here&rsquo;s the machinery, stage by stage.</p>
<h4 id="configurations-buckets-of-dependencies">Configurations: buckets of dependencies</h4>
<p>Every dependency you declare goes into a named <strong>configuration</strong> — a bucket with specific rules about where those dependencies are visible:</p>
<pre tabindex="0"><code>dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-web:3.3.1&#39;
    api &#39;com.fasterxml.jackson.core:jackson-databind:2.17.1&#39;
    compileOnly &#39;org.projectlombok:lombok:1.18.32&#39;
    annotationProcessor &#39;org.projectlombok:lombok:1.18.32&#39;
    runtimeOnly &#39;org.postgresql:postgresql:42.7.3&#39;
    testImplementation &#39;org.junit.jupiter:junit-jupiter:5.10.2&#39;
}
</code></pre><p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-05.png"></p>
<p>The implementation vs api distinction matters in multi-module builds: implementation dependencies don&rsquo;t appear on the compile classpath of modules that depend on yours. That means fewer recompilations when they change, and a cleaner API boundary. Default to implementation; reach for api only when a type from that dependency appears in your public method signatures.</p>
<h4 id="the-resolution-stages">The resolution stages</h4>
<p>Declared dependencies are just coordinates on paper. Resolution is the process of turning them into actual files, and it happens <strong>lazily</strong> — only when a task first needs a classpath (typically compileJava during Execution). The stages:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-06.png"></p>
<p><strong>Stage 1 — Graph construction.</strong> For each declared coordinate (group:name:version), Gradle queries your repositories <em>in the order they&rsquo;re declared</em> and downloads only the <em>metadata</em> first: the POM file or Gradle Module Metadata. That metadata lists the dependency&rsquo;s own dependencies, so Gradle recursively walks outward until the full transitive graph is known. No JARs have been downloaded yet — just descriptors.</p>
<p><strong>Stage 2 — Conflict resolution.</strong> Real dependency graphs always contain the same library requested at different versions (your app wants Jackson 2.17, some transitive path wants 2.15). Gradle’s default strategy is <em>highest version wins</em> — unlike Maven, which picks the “nearest” declaration in the tree. This default is usually right, but you can override it:</p>
<pre tabindex="0"><code>configurations.all {
    resolutionStrategy {
        // hard-pin a version, overriding everything
        force &#39;com.fasterxml.jackson.core:jackson-databind:2.17.1&#39;
        // or turn conflicts into build failures
        failOnVersionConflict()
    }
}
</code></pre><p><strong>Stage 3 — Constraints and rules.</strong> Before the graph is finalized, Gradle applies platforms (BOMs), version constraints, exclusions, and dependency substitutions. This is how Spring Boot keeps hundreds of libraries version-aligned with one line:</p>
<pre tabindex="0"><code>dependencies {
    implementation platform(&#39;org.springframework.boot:spring-boot-dependencies:3.3.1&#39;)
    // now version numbers can be omitted — the BOM supplies them
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
}
</code></pre><p>Exclusions cut unwanted transitive branches at the declaration site:</p>
<pre tabindex="0"><code>implementation(&#39;org.springframework.boot:spring-boot-starter-web&#39;) {
    exclude group: &#39;org.springframework.boot&#39;, module: &#39;spring-boot-starter-tomcat&#39;
}
</code></pre><p><strong>Stage 4 — Artifact download.</strong> Only now, with the final graph settled, does Gradle download the actual JAR files — and only for modules that survived resolution. Everything lands in the shared cache at ~/.gradle/caches/modules-2, keyed by checksum, shared across all projects on your machine. A dependency is downloaded once, ever, per machine (dynamic versions like 1.+ and snapshots get re-checked periodically).</p>
<p><strong>Stage 5 — Classpath assembly.</strong> The resolved files are ordered into a classpath and handed to whichever task requested them: compileClasspath to javac, runtimeClasspath to the test JVM, and so on.</p>
<h4 id="debugging-resolution">Debugging resolution</h4>
<p>Two commands answer 90% of “why is this version on my classpath” questions:</p>
<pre tabindex="0"><code># full resolved tree per configuration
gradle dependencies --configuration runtimeClasspath
# trace one library: who pulls it in, which versions were requested,
# and why the winner won
gradle dependencyInsight --dependency jackson-databind
</code></pre><h3 id="part-4-what-a-jar-file-reallyis">Part 4: What a JAR File Really Is</h3>
<p>The output of the jar task is nothing exotic — it&rsquo;s a ZIP archive with an agreed-upon internal layout. Rename it to .zip and any archive tool will open it:</p>
<pre tabindex="0"><code>myapp.jar
├── META-INF/
│   └── MANIFEST.MF          ← metadata
├── com/example/App.class     ← bytecode, path = package name
├── com/example/util/Helper.class
└── application.properties    ← resources sit alongside classes
</code></pre><p>The manifest is a plain-text key-value file. The line that matters most for a runnable JAR:</p>
<pre tabindex="0"><code>Manifest-Version: 1.0
Main-Class: com.example.App
</code></pre><p>One important caveat: the plain jar task packages <em>only your classes</em>. Your dependencies are not inside. For a self-contained deployable artifact you need a fat JAR — with the Shadow plugin, or Spring Boot&rsquo;s bootJar, which nests dependency JARs and adds its own launcher.</p>
<pre tabindex="0"><code>plugins {
    id &#39;com.github.johnrengelman.shadow&#39; version &#39;8.1.1&#39;
}
// gradle shadowJar → one JAR containing everything
</code></pre><h3 id="part-5-how-the-jvm-reads-yourjar">Part 5: How the JVM Reads Your JAR</h3>
<p>You run java -jar myapp.jar. Here&rsquo;s the sequence, and it&rsquo;s more interesting than it looks.</p>
<h4 id="step-1the-launcher-reads-themanifest">Step 1 — The launcher reads the manifest</h4>
<p>The java launcher opens the JAR as a ZIP, reads META-INF/MANIFEST.MF, and extracts Main-Class. Note a classic gotcha: with -jar, any -cp argument on the command line is <strong>silently ignored</strong>. The JAR itself (plus manifest Class-Path entries) becomes the entire application classpath.</p>
<h4 id="step-2the-classloader-hierarchy">Step 2 — The classloader hierarchy</h4>
<p>Classes are loaded by a chain of classloaders using <strong>parent-first delegation</strong>: every load request travels up to the top of the chain first, and only comes back down if no ancestor can satisfy it.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-source-to-running-jvm-how-gradle-builds-your-java-application/image-07.png"></p>
<h4 id="step-3lazyloading">Step 3 — Lazy loading</h4>
<p>The JVM does <strong>not</strong> load every class in the JAR at startup. A class is loaded the first time it’s actively used — instantiated, statically referenced, reflected upon. This is why a missing transitive dependency can let your app boot cleanly and then blow up with NoClassDefFoundError twenty minutes later, the first time a rarely-used code path runs.</p>
<h4 id="step-4load-link-initialize">Step 4 — Load, Link, Initialize</h4>
<p>Per the JVM specification, every class goes through three phases before it’s usable:</p>
<ol>
<li><strong>Loading</strong> — the class bytes are read from the JAR, parsed, and a Class object is created in metaspace.</li>
<li><strong>Linking</strong> — three sub-steps:
<ul>
<li>Verification: the bytecode is checked for structural and type safety (stack shapes, valid jumps, access rules).</li>
<li>Preparation: static fields are allocated and set to defaults — 0, null, false. Not your values yet.</li>
<li>Resolution: symbolic references to other classes, methods, and fields are resolved to concrete ones (often lazily).</li>
</ul>
</li>
<li><strong>Initialization</strong> — static initializer blocks and static field assignments run, in source order, exactly once, on first active use. This is the point where static { &hellip; } blocks fire.</li>
</ol>
<h3 id="step-5main-and-thejit">Step 5 — main() and the JIT</h3>
<p>Once Main-Class is loaded, linked, and initialized, the JVM invokes main(String[] args) on the main thread. Execution starts in the interpreter. As methods get hot — called thousands of times — the JIT compiler (tiered: C1 first for fast, lightly-optimized code, then C2 for aggressively optimized code) compiles them to native machine code. That&rsquo;s why JVM applications famously get <em>faster</em> after warm-up.</p>
<h3 id="the-wholepicture">The Whole Picture</h3>
<pre tabindex="0"><code>src/main/java  ──javac──▶  build/classes  ──jar──▶  myapp.jar
                                                        │
                                             java -jar myapp.jar
                                                        │
                       read MANIFEST.MF → find Main-Class
                                                        │
                 classloader delegation (parent-first)
                                                        │
                 Load → Link (verify/prepare/resolve) → Init
                                                        │
                            main() runs, classes load lazily,
                            JIT compiles hot paths as you go
</code></pre><p>None of this is knowledge you need daily. But the day your build takes four minutes instead of forty seconds, or two versions of the same library fight on your classpath, or a class mysteriously fails to load in production — this is the map you’ll be reading. Better to have seen it before that day arrives.</p>
]]></content:encoded></item><item><title>Claude Skills: The Reusable Knowledge Modules That Make AI Truly Powerful</title><link>https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/</link><pubDate>Sun, 21 Jun 2026 15:26:54 +0000</pubDate><guid>https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/</guid><description>&lt;p>&lt;em>How Claude loads specialised instructions on demand — and how you can write your own to transform workflows forever.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-01.png">&lt;/p>
&lt;p>Every time you open Claude and ask it to create a Word document, build a data analysis pipeline, or write presentation slides — something invisible happens first. Claude scans a directory of &lt;em>skills&lt;/em>, finds the most relevant one, reads it, and only then starts working.&lt;/p>
&lt;p>This system is called &lt;strong>Claude Skills&lt;/strong>. It is one of the most underappreciated features in the entire Claude ecosystem. And once you understand how it works, you will see why it changes everything about how you build AI-powered workflows.&lt;/p></description><content:encoded><![CDATA[<p><em>How Claude loads specialised instructions on demand — and how you can write your own to transform workflows forever.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-01.png"></p>
<p>Every time you open Claude and ask it to create a Word document, build a data analysis pipeline, or write presentation slides — something invisible happens first. Claude scans a directory of <em>skills</em>, finds the most relevant one, reads it, and only then starts working.</p>
<p>This system is called <strong>Claude Skills</strong>. It is one of the most underappreciated features in the entire Claude ecosystem. And once you understand how it works, you will see why it changes everything about how you build AI-powered workflows.</p>
<blockquote>
<p>“Skills are how Claude learns domain-specific workflows without retraining. They are runtime instructions, not model weights.”</p></blockquote>
<h3 id="what-is-askill">What Is a Skill?</h3>
<p>A skill is a directory containing a SKILL.md file (and optionally supporting assets, scripts, and reference documents). When Claude starts handling a request, it reads the <em>name</em> and <em>description</em> of every available skill. If a description matches the current task, Claude loads the full SKILL.md body and follows its instructions.</p>
<p>Think of it like a context-sensitive manual. When a carpenter picks up a dovetail saw, they know exactly how to use it — not because they memorised every detail about every tool upfront, but because they learned the right technique for the right moment. Skills work the same way for Claude.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-02.png"></p>
<h3 id="the-three-layer-loadingsystem">The Three-Layer Loading System</h3>
<p>Skills use progressive disclosure — Claude never loads everything at once. There are three distinct layers, each loaded only as needed:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-03.png"></p>
<p>This is elegant engineering. Layer 1 is tiny — just enough for Claude to decide <em>whether</em> a skill applies. Layer 2 gives full instructions once relevant. Layer 3 provides deep reference material only when needed, keeping context usage lean.</p>
<h3 id="anatomy-of-a-skill-directory">Anatomy of a Skill Directory</h3>
<p>A skill is a folder with a predictable structure. Here is the full layout:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-04.png"></p>
<p>Only SKILL.md is required. Everything else is optional and only loaded when the skill itself explicitly references it.</p>
<h3 id="inside-skillmdthe-configuration-fields">Inside SKILL.md — The Configuration Fields</h3>
<p>The file starts with a YAML frontmatter block (between &mdash; delimiters), followed by markdown instructions. Here is every supported field:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-05.png"></p>
<blockquote>
<p><strong>KEY INSIGHT</strong>The description field is the most important part of any skill. Claude decides whether to load your skill based solely on this field — the body never loads unless the description matches. Write it with real triggers, real contexts, and a slight &ldquo;push&rdquo; to combat under-triggering.</p></blockquote>
<h3 id="a-minimal-skillstep-bystep">A Minimal Skill — Step by Step</h3>
<p>Let’s build a real skill from scratch. Imagine you regularly need to convert raw PostgreSQL query results into nicely formatted Markdown reports. Here is how to package that as a skill.</p>
<h4 id="step-1create-the-directory">Step 1 — Create the directory</h4>
<pre tabindex="0"><code>mkdir pg-report-skill
cd pg-report-skill
touch SKILL.md
</code></pre><h4 id="step-2write-the-frontmatter">Step 2 — Write the frontmatter</h4>
<pre tabindex="0"><code>---
name: pg-report
description: |
  Converts raw PostgreSQL query results into formatted Markdown reports
  with tables, summary stats, and query metadata. Use whenever the user
  pastes SQL output, mentions &#34;format this query result&#34;, or wants to
  turn database output into a readable report or document. Also trigger
  when user asks to summarise a dataset from a .csv or .tsv that looks
  like a DB export.
---
</code></pre><blockquote>
<p><strong>TIP: WRITE THE DESCRIPTION LIKE A TRIGGER RULESET</strong>Notice how the description lists multiple phrasings (“pastes SQL output”, “format this query result”, “summarise a dataset”). This breadth is intentional — Claude uses fuzzy matching, so covering several natural phrasings dramatically improves trigger reliability.</p></blockquote>
<h4 id="step-3write-the-skillbody">Step 3 — Write the skill body</h4>
<pre tabindex="0"><code># PostgreSQL Report Formatter
## What this skill does
Formats raw PostgreSQL tabular output into clean Markdown reports.
## Required output sections
1. A ## Summary block: row count, column count, date range (if present)
2. A ## Data block: full Markdown table with aligned columns
3. A ## Observations block: 3-5 key insights from the data
## Formatting rules
- Numeric columns: right-align, add thousand separators
- Date columns: format as YYYY-MM-DD
- NULL values: render as `—` (em dash), never blank
- Column headers: Title Case
## Example input
</code></pre><p>id | user_name | total_orders | revenue
&mdash;-+&mdash;&mdash;&mdash;&ndash;+&mdash;&mdash;&mdash;&mdash;&ndash;+&mdash;&mdash;&mdash;
1 | alice     |           12 | 4500.00
2 | bob       |            3 |  890.50</p>
<pre tabindex="0"><code>## Example output
### Summary
- **Rows:** 2  |  **Columns:** 4  |  **Total revenue:** $5,390.50
### Data
| ID | User Name | Total Orders | Revenue |
|----|-----------|-------------:|--------:|
|  1 | Alice     |           12 | $4,500.00 |
|  2 | Bob       |            3 |   $890.50 |
</code></pre><h3 id="step-4add-a-reference-file-optional">Step 4 — Add a reference file (optional)</h3>
<p>If your skill needs to handle multiple database types — PostgreSQL, MySQL, SQLite — you can create a references/ folder and have the skill body instruct Claude to load the right one:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-skills-the-reusable-knowledge-modules-that-make-ai-truly-powerful/image-06.png"></p>
<p>In your SKILL.md body, you then write:</p>
<pre tabindex="0"><code>## Database-specific rules
Detect which database produced the output from syntax clues.
- If `||` concatenation → read references/sqlite.md
- If `AUTO_INCREMENT` → read references/mysql.md
- Default → read references/postgres.md
</code></pre><p>Claude will read only the relevant file, keeping the context window lean and responses accurate.</p>
<h3 id="real-world-example-the-built-in-docxskill">Real-World Example: The Built-in DOCX Skill</h3>
<p>Claude ships with several built-in skills. The docx skill is a great example of production-quality configuration. Here is its description field, distilled:</p>
<pre tabindex="0"><code>description: |
  Use this skill whenever the user wants to create, read, edit, or
  manipulate Word documents (.docx files). Triggers include: any
  mention of &#39;Word doc&#39;, &#39;word document&#39;, &#39;.docx&#39;, or requests to
  produce professional documents with formatting like tables of
  contents, headings, page numbers, or letterheads. Also use when
  extracting content from .docx files, inserting images, performing
  find-and-replace, working with tracked changes, or converting
  content into a polished Word document.
</code></pre><p>Notice the pattern: the description is almost a checklist. Every plausible user phrasing that should trigger the skill is listed. This is intentional — Claude has a natural tendency to <em>under-trigger</em> skills, so descriptions are deliberately thorough.</p>
<blockquote>
<p><strong>WARNING: AVOID GENERIC DESCRIPTIONS</strong></p></blockquote>
<blockquote>
<p>A description like “Helps with documents” will rarely trigger. Claude needs enough specificity to distinguish your skill from general knowledge. Name the exact task, the exact file types, and the exact user phrases that should activate it.</p></blockquote>
<h3 id="the-progressive-disclosure-principle">The Progressive Disclosure Principle</h3>
<p>The most important architectural decision in skill design is knowing what goes where. The three-layer rule is simple:</p>
<h4 id="1-description--triggering-logiconly">1. Description = triggering logic only</h4>
<p>Never put instructions in the description. It exists solely to help Claude decide whether to load the skill. Keep it focused on “when to use” language, not “how to do”.</p>
<h4 id="2-skillmd-body--coreworkflow">2. SKILL.md body = core workflow</h4>
<p>Put the main instructions, output format rules, required sections, formatting guidelines, and examples here. Target under 500 lines. If you need more, move deep content into reference files.</p>
<h4 id="3-references--scripts--deep-knowledge">3. References + scripts = deep knowledge</h4>
<p>Detailed documentation, database-specific rules, platform-specific templates, and executable scripts go in subfolders. The SKILL.md body tells Claude when to read them.</p>
<h3 id="writing-descriptions-that-actuallytrigger">Writing Descriptions That Actually Trigger</h3>
<p>The description field is the hardest part to get right. Here is a battle-tested template:</p>
<pre tabindex="0"><code>description: |
  [What the skill does in one sentence].
  Use this skill when: [scenario A], [scenario B], [scenario C].
  Also trigger when the user [natural phrase], [natural phrase],
  or mentions [keyword / file type / tool name].
  Do NOT use for [explicit exclusion to avoid false positives].
</code></pre><p>And a concrete example for a “Java Code Review” skill:</p>
<pre tabindex="0"><code>description: |
  Reviews Java source files for correctness, performance, thread
  safety, and style — producing structured inline feedback.
  Use whenever the user shares Java code and asks for review,
  feedback, improvements, or a PR review. Also trigger on phrases
  like &#34;check my code&#34;, &#34;what&#39;s wrong here&#34;, or &#34;how can I improve
  this&#34; when the code is Java. Trigger on .java files even without
  an explicit review request.
  Do NOT use for Kotlin, Scala, or general architecture questions
  without code attached.
</code></pre><h3 id="when-skills-usescripts">When Skills Use Scripts</h3>
<p>For deterministic or repetitive transformations — file conversions, data extraction, package generation — skills can include executable scripts. Claude reads the script path from the SKILL.md body, runs it via the bash tool, and uses the output.</p>
<p>The built-in docx skill, for example, uses a Python script to write .docxfiles using the python-docx library. The skill body contains step-by-step instructions for calling the script correctly — Claude follows them at runtime.</p>
<pre tabindex="0"><code>## Creating a DOCX file
1. Generate Python code using python-docx from the template at
   scripts/create_doc.py
2. Run: python scripts/create_doc.py output.docx
3. Copy result to /mnt/user-data/outputs/
4. Present to user with present_files tool
</code></pre><h3 id="testing-yourskill">Testing Your Skill</h3>
<p>Before deploying, write 5–10 test prompts that should trigger your skill, and 3–5 that should not. Run Claude with the skill installed and check whether it loads correctly.</p>
<p>Good test prompts for a “pg-report” skill:</p>
<pre tabindex="0"><code># Should trigger:
&#34;Here&#39;s my query result, can you format it nicely?&#34;
&#34;Turn this SQL output into a report&#34;
&#34;I exported this CSV from my database, summarise it&#34;
# Should NOT trigger:
&#34;How do I write a JOIN query?&#34;
&#34;What is normalisation in databases?&#34;
</code></pre><blockquote>
<p><strong>PRO TIP: QUANTITATIVE EVALUATION</strong>If you are building skills at scale, the skill-creator meta-skill includes an automated optimization loop. It splits your test set 60/40 (train/held-out), runs Claude against each case multiple times, proposes description improvements, and iterates up to 5 rounds — outputting the best-performing description with before/after scores.</p></blockquote>
<h3 id="packaging-and-sharingskills">Packaging and Sharing Skills</h3>
<p>A finished skill can be packaged into a .skill file for distribution. Anyone can install it by dropping the file into their skills directory. The package script handles archiving:</p>
<pre tabindex="0"><code>python -m scripts.package_skill path/to/pg-report-skill/
# → outputs pg-report-skill.skill
</code></pre><h3 id="the-biggerpicture">The Bigger Picture</h3>
<p>Skills are not just a convenience feature. They are the mechanism by which Claude becomes <em>domain-aware</em> without retraining. A legal firm can build a contract-review skill. A data team can build a dashboard-generation skill. A game studio can build a Godot export skill. Each one runs inside the same Claude — just with different runtime instructions.</p>
<p>The architecture is deliberately modular. Skills compose. A single request might trigger the pdf-reading skill to extract content, then the docx skill to write the output. Each stays in its lane, and Claude orchestrates.</p>
<blockquote>
<p>“You don’t teach Claude new tricks by changing the model. You teach it by writing better skills.”</p></blockquote>
<p>For solo developers and small teams, this is genuinely powerful. You can codify your best workflows once, as a skill, and they run reliably on every request — without prompting, without repetition, and without hoping Claude happens to “know” your preferred format.</p>
<p>Start small. Pick one workflow you repeat every week. Write a 30-line SKILL.md. Test it on five prompts. Then iterate. That is all it takes to start building your own personalised AI toolkit.</p>
<p><strong>RESOURCES</strong>The skills discussed in this article — including docx, pdf, pptx, xlsx, and skill-creator — are built into Claude&rsquo;s computer-use environment. You can explore them at /mnt/skills/public/ in any Claude session with computer use enabled.</p>
]]></content:encoded></item><item><title>MCP: The Protocol That Finally Taught AI to Stop Being Useless</title><link>https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/</link><pubDate>Mon, 15 Jun 2026 20:00:06 +0000</pubDate><guid>https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/</guid><description>&lt;p>&lt;em>A beginner’s guide to Model Context Protocol — the thing that lets your AI assistant actually do stuff, not just talk about doing stuff.&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-01.png">&lt;/p>
&lt;p>Picture this: you’ve got the most brilliant assistant in the world. Knows everything. Speaks 50 languages. Can explain quantum physics and write poetry at the same time. But here’s the catch — &lt;strong>they’re locked in a glass box&lt;/strong>. They can see your computer screen. They can describe what they’d do if they could reach it. But touch anything? Nope. Read a file? Uh-uh. Check your calendar? Dream on.&lt;/p></description><content:encoded><![CDATA[<p><em>A beginner’s guide to Model Context Protocol — the thing that lets your AI assistant actually do stuff, not just talk about doing stuff.</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-01.png"></p>
<p>Picture this: you’ve got the most brilliant assistant in the world. Knows everything. Speaks 50 languages. Can explain quantum physics and write poetry at the same time. But here’s the catch — <strong>they’re locked in a glass box</strong>. They can see your computer screen. They can describe what they’d do if they could reach it. But touch anything? Nope. Read a file? Uh-uh. Check your calendar? Dream on.</p>
<p>That was AI assistants until very recently. Incredibly smart, utterly helpless. Like hiring Einstein to organize your sock drawer and then refusing to let him into the room.</p>
<p>Enter <strong>MCP — the Model Context Protocol</strong>. The thing that breaks the glass and lets your AI assistant actually <em>do the thing</em>.</p>
<blockquote>
<p><em>MCP is essentially a USB standard for AI. Before USB, every device needed its own special plug. Now there’s one protocol, and everything just… works.</em></p></blockquote>
<p>By the end of this article you’ll understand what MCP is, why it exists, how it works, and — most importantly — you’ll build your own working MCP server from scratch and connect it to Claude. Buckle up.</p>
<h3 id="a-brief-history-of-ais-helplessness-problem">A Brief History of AI’s Helplessness Problem</h3>
<p>To understand why MCP is a big deal, we need to appreciate just how spectacularly bad the situation was before it.</p>
<blockquote>
<p><strong>EARLY 2023</strong></p></blockquote>
<blockquote>
<p>Large Language Models (LLMs) become genuinely impressive. Everyone rushes to build assistants. The dream: AI that manages your life. The reality: AI that describes your life back to you very eloquently.</p></blockquote>
<blockquote>
<p><strong>MID 2023</strong></p></blockquote>
<blockquote>
<p>“Function calling” arrives. Now you can tell the AI: “hey, you can call these functions.” Progress! But every app invents its own format. A plugin for ChatGPT works nowhere else. A LangChain tool chain doesn’t talk to Claude. It’s the Wild West, but with JSON.</p></blockquote>
<blockquote>
<p><strong>LATE 2023–2024</strong></p></blockquote>
<blockquote>
<p>Developers build increasingly elaborate custom integrations. Everyone reinvents the wheel. Slightly differently. Slightly wrongly. A thousand incompatible “AI tool” standards bloom. This is the Tower of Babel, Silicon Valley edition.</p></blockquote>
<blockquote>
<p><strong>NOVEMBER 2024</strong></p></blockquote>
<blockquote>
<p><strong>Anthropic releases the Model Context Protocol</strong>. An open standard. One protocol to rule them all. The developer community collectively exhales. Finally.</p></blockquote>
<blockquote>
<p><strong>2025</strong></p></blockquote>
<blockquote>
<p>MCP adoption explodes. OpenAI, Google, Microsoft all announce support. Hundreds of MCP servers appear on GitHub. Your AI can now read files, browse the web, query databases, control your computer, send Slack messages, and order pizza. (Well, almost.)</p></blockquote>
<p>The key insight Anthropic had was embarrassingly simple: <strong>instead of every AI model learning to speak every tool’s language, make every tool learn one common language</strong>. The result is MCP.</p>
<h3 id="what-is-mcp-actually">What Is MCP, Actually?</h3>
<p>MCP stands for <strong>Model Context Protocol</strong>. It’s an open standard (meaning anyone can use it, free of charge) that defines how AI models communicate with external tools and data sources.</p>
<p>Think of it as a formal language that both sides agree to speak. The AI model is one speaker. Your tool (a file reader, a database, a weather API, your smart fridge) is the other speaker. MCP is the language between them.</p>
<blockquote>
<p><strong>The one-sentence explanation:</strong><br>
MCP lets AI models talk to <em>any</em> tool through a single standardized protocol, so you write the connector once and it works everywhere.</p></blockquote>
<p>Technically, MCP is a client-server architecture built on top of <a href="https://www.jsonrpc.org/specification"><strong>JSON-RPC</strong></a> <strong>2.0</strong>. Don’t be scared by that. It means messages are passed as readable JSON objects, and there’s a strict format for how requests and responses look. Clean, simple, predictable.</p>
<h3 id="the-architecture-threeamigos">The Architecture: Three Amigos</h3>
<p>MCP has three main characters. Let’s meet them:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-02.png"></p>
<h4 id="the-host">The Host</h4>
<blockquote>
<p>The application running the AI. Could be Claude Desktop, VS Code with Copilot, or your own app. The host is the conductor — it decides when the AI needs a tool and coordinates everything.</p></blockquote>
<h4 id="the-client">The Client</h4>
<blockquote>
<p>Technically a component <em>inside</em> the host. It handles the actual MCP communication — sends requests to servers, gets back results, feeds them to the AI. You don’t usually build this yourself.</p></blockquote>
<h4 id="the-server">The Server</h4>
<blockquote>
<p><strong>This is what you build.</strong> An MCP server exposes capabilities — tools, resources, and prompts — that the AI can use. Your server could wrap a database, a REST API, the local file system, or anything else. This is where the magic you write lives.</p></blockquote>
<h3 id="core-concepts-tools-resources-prompts">Core Concepts: Tools, Resources &amp; Prompts</h3>
<p>MCP servers expose three types of things to AI models. Think of these as different kinds of “capabilities”:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-03.png"></p>
<p>For most real-world use cases you’ll mainly deal with <strong>Tools</strong>. That’s where the action is.</p>
<h3 id="how-they-talk-the-mcp-messageformat">How They Talk: The MCP Message Format</h3>
<p>MCP messages are JSON objects following the <a href="https://www.jsonrpc.org/specification"><strong>JSON-RPC 2.0</strong></a> specification. Every single message — whether it’s a greeting, a tool call, or an error — follows the same envelope structure. Once you learn the envelope, the rest is just filling in the blanks.</p>
<h4 id="the-anatomy-of-everymessage">The Anatomy of Every Message</h4>
<p>All JSON-RPC messages share the same top-level fields. Let’s dissect them:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-04.png"></p>
<h3 id="phase-1the-handshake-initialize">Phase 1 — The Handshake: initialize</h3>
<p>Before any tools are called, the host and server must introduce themselves. This is the very first exchange that happens when Claude Desktop spawns your server process. Think of it as two people shaking hands and agreeing on a common language before getting to business.</p>
<p>1A. HOST → SERVER: INITIALIZE REQUEST</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 0,
  &#34;method&#34;: &#34;initialize&#34;,
  &#34;params&#34;: {
    &#34;protocolVersion&#34;: &#34;2024-11-05&#34;,        // MCP spec version the host supports
    &#34;capabilities&#34;: {
      &#34;roots&#34;: { &#34;listChanged&#34;: true },    // Host can notify server about root changes
      &#34;sampling&#34;: {}                         // Host supports LLM sampling requests
    },
    &#34;clientInfo&#34;: {
      &#34;name&#34;: &#34;Claude Desktop&#34;,
      &#34;version&#34;: &#34;0.7.0&#34;
    }
  }
}
</code></pre><p>1B. SERVER → HOST: INITIALIZE RESPONSE</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 0,                                    // Same id — this is the matching reply
  &#34;result&#34;: {
    &#34;protocolVersion&#34;: &#34;2024-11-05&#34;,        // Server confirms the version
    &#34;capabilities&#34;: {
      &#34;tools&#34;: { &#34;listChanged&#34;: false }    // Server exposes tools (and won&#39;t hot-reload them)
    },
    &#34;serverInfo&#34;: {
      &#34;name&#34;: &#34;my-first-mcp-server&#34;,
      &#34;version&#34;: &#34;1.0.0&#34;
    }
  }
}
</code></pre><p>1C. HOST → SERVER: INITIALIZED NOTIFICATION (NO REPLY EXPECTED)</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;method&#34;: &#34;notifications/initialized&#34;
  // No &#34;id&#34; field — this is a notification, not a request.
  // Notifications are fire-and-forget; the server never replies.
}
</code></pre><p>After this three-step handshake, the connection is live and both sides know what the other can do. Now the real fun begins.</p>
<h3 id="phase-2tool-discovery-toolslist">Phase 2 — Tool Discovery: tools/list</h3>
<p>The host immediately asks: “OK, what tools do you have?” This lets the AI know its full menu of capabilities before the user types a single character.</p>
<p>2A. HOST → SERVER: TOOLS/LIST REQUEST</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 1,
  &#34;method&#34;: &#34;tools/list&#34;
  // No &#34;params&#34; needed — just asking for the full list.
}
</code></pre><p>2B. SERVER → HOST: TOOLS/LIST RESPONSE (WITH FULL INPUTSCHEMA)</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 1,
  &#34;result&#34;: {
    &#34;tools&#34;: [
      {
        &#34;name&#34;: &#34;get_weather&#34;,
        &#34;description&#34;: &#34;Get current weather for any city.&#34;,
        &#34;inputSchema&#34;: {          // Standard JSON Schema object
          &#34;type&#34;: &#34;object&#34;,
          &#34;properties&#34;: {
            &#34;city&#34;: {
              &#34;type&#34;: &#34;string&#34;,
              &#34;description&#34;: &#34;City name, e.g. Belfast&#34;
            },
            &#34;unit&#34;: {
              &#34;type&#34;: &#34;string&#34;,
              &#34;enum&#34;: [&#34;celsius&#34;, &#34;fahrenheit&#34;],   // Constrained to specific values
              &#34;default&#34;: &#34;celsius&#34;
            }
          },
          &#34;required&#34;: [&#34;city&#34;]            // &#34;unit&#34; is optional, &#34;city&#34; is not
        }
      },
      {
        &#34;name&#34;: &#34;search_database&#34;,
        &#34;description&#34;: &#34;Search products by keyword with optional filters.&#34;,
        &#34;inputSchema&#34;: {
          &#34;type&#34;: &#34;object&#34;,
          &#34;properties&#34;: {
            &#34;query&#34;: { &#34;type&#34;: &#34;string&#34; },
            &#34;limit&#34;: {
              &#34;type&#34;: &#34;integer&#34;,
              &#34;minimum&#34;: 1,
              &#34;maximum&#34;: 100,
              &#34;default&#34;: 10
            },
            &#34;tags&#34;: {
              &#34;type&#34;: &#34;array&#34;,              // Arrays of primitives are fine
              &#34;items&#34;: { &#34;type&#34;: &#34;string&#34; }
            }
          },
          &#34;required&#34;: [&#34;query&#34;]
        }
      }
    ]
  }
}
</code></pre><blockquote>
<p>The inputSchema is standard <a href="https://json-schema.org/understanding-json-schema/about"><strong>JSON Schema</strong></a>. You can use type, enum, minimum, maximum, default, array, nested object — any JSON Schema feature. The AI model reads this schema and uses it to build valid arguments automatically. Good schemas = fewer mistakes by the AI.</p></blockquote>
<h3 id="phase-3tool-execution-toolscall">Phase 3 — Tool Execution: tools/call</h3>
<p>The user says something. The AI decides a tool is needed. It fires a tools/callwith the arguments it constructed from the schema.</p>
<p>3A. SIMPLE CALL — ONE STRING ARGUMENT</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 2,
  &#34;method&#34;: &#34;tools/call&#34;,
  &#34;params&#34;: {
    &#34;name&#34;: &#34;get_weather&#34;,
    &#34;arguments&#34;: {
      &#34;city&#34;: &#34;Belfast&#34;           // Only the required field; &#34;unit&#34; uses its default
    }
  }
}
</code></pre><p>3B. COMPLEX CALL — MULTIPLE TYPES INCLUDING ARRAY</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 3,
  &#34;method&#34;: &#34;tools/call&#34;,
  &#34;params&#34;: {
    &#34;name&#34;: &#34;search_database&#34;,
    &#34;arguments&#34;: {
      &#34;query&#34;: &#34;wireless headphones&#34;,
      &#34;limit&#34;: 5,                  // integer
      &#34;tags&#34;: [&#34;bluetooth&#34;, &#34;noise-cancelling&#34;]  // array
    }
  }
}
</code></pre><p>3C. SUCCESS RESPONSE — TEXT CONTENT</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 2,
  &#34;result&#34;: {
    &#34;content&#34;: [
      {
        &#34;type&#34;: &#34;text&#34;,
        &#34;text&#34;: &#34;Current weather in Belfast: 12°C, wind 18 km/h&#34;
      }
    ],
    &#34;isError&#34;: false          // Optional. Explicitly marks success.
  }
}
</code></pre><p>3D. SUCCESS RESPONSE — MULTIPLE CONTENT BLOCKS</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 3,
  &#34;result&#34;: {
    &#34;content&#34;: [
      {
        &#34;type&#34;: &#34;text&#34;,
        &#34;text&#34;: &#34;Found 2 results for &#39;wireless headphones&#39;:&#34;
      },
      {
        &#34;type&#34;: &#34;text&#34;,         // Second block — same message, more content
        &#34;text&#34;: &#34;1. Sony WH-1000XM5 — £279\n2. Bose QC45 — £249&#34;
      }
      // Could also include &#34;image&#34; blocks with base64 data,
      // or &#34;resource&#34; blocks referencing URIs
    ]
  }
}
</code></pre><h3 id="phase-4when-things-go-wrong-error-responses">Phase 4 — When Things Go Wrong: Error Responses</h3>
<p>There are two distinct flavours of error in MCP. Don’t mix them up — they mean very different things.</p>
<p>PROTOCOL ERROR — THE REQUEST ITSELF WAS MALFORMED</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 4,
  &#34;error&#34;: {                         // &#34;error&#34; field, NOT &#34;result&#34;
    &#34;code&#34;: -32602,                  // Standard JSON-RPC error code
    &#34;message&#34;: &#34;Invalid params&#34;,
    &#34;data&#34;: {
      &#34;details&#34;: &#34;Required field &#39;city&#39; is missing&#34;
    }
  }
}
</code></pre><p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-05.png"></p>
<p>TOOL-LEVEL ERROR — THE TOOL RAN BUT THE OPERATION FAILED</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 5,
  &#34;result&#34;: {                        // Still &#34;result&#34;, NOT &#34;error&#34; !
    &#34;content&#34;: [
      {
        &#34;type&#34;: &#34;text&#34;,
        &#34;text&#34;: &#34;City &#39;Narnia&#39; not found in geocoding database.&#34;
      }
    ],
    &#34;isError&#34;: true               // Flag tells the AI: tool ran, but reported failure
  }
}
</code></pre><blockquote>
<p><strong>Key distinction:</strong> A <em>protocol error</em> (using the &ldquo;error&rdquo; field) means the communication broke — bad JSON, unknown method, missing required fields. A <em>tool error</em> (using &ldquo;result&rdquo; with &ldquo;isError&rdquo;: true) means the tool ran fine but the operation itself failed — user not found, city doesn&rsquo;t exist, API returned 404. Always use isError: true for business logic failures so the AI can read the message and react intelligently.</p></blockquote>
<h3 id="bonus-resource-reads-and-notifications">Bonus: Resource Reads and Notifications</h3>
<p>Tools aren’t the only thing MCP can do. Here’s what a <strong>resource read</strong> looks like — useful when you want to expose file contents or database records the AI can browse:</p>
<p>RESOURCES/READ REQUEST AND RESPONSE</p>
<pre tabindex="0"><code>// Request
{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 6,
  &#34;method&#34;: &#34;resources/read&#34;,
  &#34;params&#34;: {
    &#34;uri&#34;: &#34;file:///home/user/notes.txt&#34;    // Resource URIs identify the data source
  }
}
// Response
{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;id&#34;: 6,
  &#34;result&#34;: {
    &#34;contents&#34;: [
      {
        &#34;uri&#34;: &#34;file:///home/user/notes.txt&#34;,
        &#34;mimeType&#34;: &#34;text/plain&#34;,
        &#34;text&#34;: &#34;Buy milk. Deploy the thing. Don&#39;t forget stand-up.&#34;
      }
    ]
  }
}
</code></pre><p>And a <strong>server notification</strong> — fire-and-forget messages the server sends without being asked (no id, no reply expected):</p>
<p>SERVER → HOST NOTIFICATION (NO REPLY)</p>
<pre tabindex="0"><code>{
  &#34;jsonrpc&#34;: &#34;2.0&#34;,
  &#34;method&#34;: &#34;notifications/tools/list_changed&#34;,
  // No &#34;id&#34; — this is a push notification.
  // The server is telling the host: &#34;Hey, my tool list changed.
  //  Please call tools/list again to refresh.&#34;
}
</code></pre><p>Clean and readable. No magic, no wizardry. Just JSON going back and forth. This predictability is exactly what makes MCP powerful — any language, any framework, any tool can implement it.</p>
<h3 id="two-ways-to-connect-stdio-vshttpsse">Two Ways to Connect: stdio vs HTTP/SSE</h3>
<p>MCP servers can communicate via two transport mechanisms:</p>
<p><strong>stdio (Standard I/O)</strong> — The simplest option. Your server reads from stdin and writes to stdout. The host app spawns the server as a subprocess. Perfect for local tools running on the same machine.</p>
<p><strong>HTTP + SSE (Server-Sent Events)</strong> — For remote servers. The client sends POST requests to the server, and the server streams events back. Use this when your MCP server lives on a different machine (a cloud server, a colleague’s computer, etc.).</p>
<blockquote>
<p>Start with <strong>stdio</strong>. It’s simpler, requires no networking setup, and is perfect for local tooling. You can always switch to HTTP later.</p></blockquote>
<h3 id="tutorial-building-serverpy-step-bystep">Tutorial: Building server.py Step by Step</h3>
<p>Enough theory. Let’s write the actual code. Below is a complete, annotated server.py that exposes three tools: a dad joke generator, a task list, and a real live weather lookup. We&rsquo;ll build it section by section so nothing is a mystery.</p>
<p>📦<strong>Install first:</strong> pip install mcp — that&rsquo;s the only dependency. Python 3.10+ required.</p>
<h4 id="part-1imports--server-instantiation">Part 1 — Imports &amp; Server Instantiation</h4>
<p>The MCP SDK ships everything you need in the mcp package. We create a Server instance and tell it our name and version — this is what Claude Desktop sees in the handshake.</p>
<p><strong>server.py — Part 1: imports and setup</strong></p>
<pre tabindex="0"><code># Standard library — no extra installs needed for HTTP calls
import asyncio
import random
import json
import urllib.request
import urllib.parse
import sys                   # needed for stderr debug output
# MCP SDK - install with: pip install mcp
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
# Create the server. The name/version appear during the MCP handshake.
# Claude Desktop displays this name in its connected servers list.
app = Server(&#34;my-first-mcp-server&#34;)
</code></pre><h4 id="part-2declaring-tools-with-applist_tools">Part 2 — Declaring Tools with @app.list_tools()</h4>
<p>This decorator registers the handler that responds to tools/list requests. The AI calls this automatically on startup to discover your full menu of capabilities. <strong>The descriptions you write here are read by the AI</strong> — write them like you&rsquo;re explaining the tool to a smart colleague, not a compiler.</p>
<p><strong>server.py — Part 2: tool declarations</strong></p>
<pre tabindex="0"><code>@app.list_tools()
async def list_tools() -&gt; list[types.Tool]:
    return [
        # ── Tool 1: dad_joke ──────────────────────────────────────
        # No arguments. The inputSchema is an empty object.
        # The AI will call this whenever the user needs cheering up.
        types.Tool(
            name=&#34;dad_joke&#34;,
            description=&#34;Returns a random, gloriously terrible dad joke. Use when morale is low.&#34;,
            inputSchema={
                &#34;type&#34;: &#34;object&#34;,
                &#34;properties&#34;: {},    # no arguments - jokes are free
                &#34;required&#34;: [],
            },
        ),
        # ── Tool 2: get_tasks ─────────────────────────────────────
        # One required string argument: username.
        # &#34;required&#34; list enforces that the AI must supply it.
        types.Tool(
            name=&#34;get_tasks&#34;,
            description=&#34;Returns the task list for a given user.&#34;,
            inputSchema={
                &#34;type&#34;: &#34;object&#34;,
                &#34;properties&#34;: {
                    &#34;username&#34;: {
                        &#34;type&#34;: &#34;string&#34;,
                        &#34;description&#34;: &#34;The username whose tasks to fetch, e.g. &#39;alice&#39;&#34;,
                    },
                },
                &#34;required&#34;: [&#34;username&#34;],
            },
        ),
        # ── Tool 3: get_weather ───────────────────────────────────
        # Calls the real Open-Meteo API. No API key required.
        # &#34;unit&#34; is optional - has a default value in the handler.
        types.Tool(
            name=&#34;get_weather&#34;,
            description=&#34;Get current weather for any city. Uses Open-Meteo (free, no API key needed).&#34;,
            inputSchema={
                &#34;type&#34;: &#34;object&#34;,
                &#34;properties&#34;: {
                    &#34;city&#34;: {
                        &#34;type&#34;: &#34;string&#34;,
                        &#34;description&#34;: &#34;City name, e.g. Belfast, Tokyo, New York&#34;,
                    },
                    &#34;unit&#34;: {
                        &#34;type&#34;: &#34;string&#34;,
                        &#34;enum&#34;: [&#34;celsius&#34;, &#34;fahrenheit&#34;],
                        &#34;description&#34;: &#34;Temperature unit. Defaults to celsius.&#34;,
                    },
                },
                &#34;required&#34;: [&#34;city&#34;],   # &#34;unit&#34; is optional - omit from required
            },
        ),
    ]
</code></pre><h4 id="part-3handling-tool-calls-with-appcall_tool">Part 3 — Handling Tool Calls with @app.call_tool()</h4>
<p>When the AI decides to invoke a tool, this handler fires. It receives the tool name and the arguments dict the AI built from your schema. Return a list of TextContent objects — even if there&rsquo;s only one. Always return a list; the protocol expects it.</p>
<p><strong>server.py — Part 3: tool logic</strong></p>
<pre tabindex="0"><code>@app.call_tool()
async def call_tool(name: str, arguments: dict) -&gt; list[types.TextContent]:
# ── dad_joke ──────────────────────────────────────────────
    if name == &#34;dad_joke&#34;:
        jokes = [
            &#34;Why don&#39;t scientists trust atoms? Because they make up everything.&#34;,
            &#34;I told my wife she was drawing her eyebrows too high. She looked surprised.&#34;,
            &#34;Why do programmers prefer dark mode? Because light attracts bugs.&#34;,
            &#34;I asked my dog what two minus two is. He said nothing.&#34;,
            &#34;Why did the MCP server go to therapy? Too many unhandled exceptions.&#34;,
        ]
        return [types.TextContent(type=&#34;text&#34;, text=random.choice(jokes))]
    # ── get_tasks ─────────────────────────────────────────────
    # In real life you&#39;d query a DB here. We use a dict for clarity.
    if name == &#34;get_tasks&#34;:
        username = arguments.get(&#34;username&#34;, &#34;&#34;).lower()
        fake_tasks = {
            &#34;alice&#34;: [&#34;Review PR #42&#34;, &#34;Update docs&#34;, &#34;Fix that bug you&#39;ve been ignoring&#34;],
            &#34;bob&#34;:   [&#34;Stand-up meeting&#34;, &#34;Drink coffee&#34;, &#34;Question all life choices&#34;],
        }
        tasks = fake_tasks.get(username, [&#34;No tasks found. Either very productive or very lazy.&#34;])
        formatted = &#34;\n&#34;.join(f&#34;{i+1}. {t}&#34; for i, t in enumerate(tasks))
        return [types.TextContent(type=&#34;text&#34;, text=f&#34;Tasks for {username}:\n{formatted}&#34;)]
    # ── get_weather ───────────────────────────────────────────
    # Two HTTP calls: geocode the city name → get weather by lat/lon.
    # Uses urllib (stdlib) so no extra dependencies.
    if name == &#34;get_weather&#34;:
        city = arguments.get(&#34;city&#34;, &#34;&#34;)
        unit = arguments.get(&#34;unit&#34;, &#34;celsius&#34;)   # default if AI omits it
        # Step 1 - turn city name into coordinates
        geo_url = (
            &#34;https://geocoding-api.open-meteo.com/v1/search&#34;
            f&#34;?name={urllib.parse.quote(city)}&amp;count=1&#34;
        )
        with urllib.request.urlopen(geo_url) as r:
            geo = json.loads(r.read())
        if not geo.get(&#34;results&#34;):
            # Tool ran fine, but the operation failed - use isError flag
            return [types.TextContent(type=&#34;text&#34;, text=f&#39;City &#34;{city}&#34; not found.&#39;)]
        result = geo[&#34;results&#34;][0]
        lat, lon, found_name = result[&#34;latitude&#34;], result[&#34;longitude&#34;], result[&#34;name&#34;]
        # Step 2 - fetch current weather for those coordinates
        wx_url = (
            &#34;https://api.open-meteo.com/v1/forecast&#34;
            f&#34;?latitude={lat}&amp;longitude={lon}&#34;
            f&#34;¤t_weather=true&amp;temperature_unit={unit}&#34;
        )
        with urllib.request.urlopen(wx_url) as r:
            wx = json.loads(r.read())
        cw = wx[&#34;current_weather&#34;]
        symbol = &#34;°C&#34; if unit == &#34;celsius&#34; else &#34;°F&#34;
        return [types.TextContent(            type=&#34;text&#34;,            text=f&#34;{found_name}: {cw[&#39;temperature&#39;]}{symbol}, wind {cw[&#39;windspeed&#39;]} km/h&#34;,        )]
    # Unknown tool - raise so MCP returns a proper protocol error
    raise ValueError(f&#34;Unknown tool: {name}&#34;)
</code></pre><h3 id="part-4wiring-up-stdio-transport">Part 4 — Wiring Up stdio Transport</h3>
<p>The final piece connects the server to the MCP host via stdin/stdout. stdio_server() returns two async streams; we hand them to app.run() and it takes care of the rest. The key thing to remember: <strong>never</strong> <strong>print() to stdout</strong> anywhere in your code — that channel belongs to MCP. Debug with sys.stderr only.</p>
<p>server.py — Part 4: entry point</p>
<pre tabindex="0"><code>async def main():
    # stdio_server() yields (read_stream, write_stream) for the MCP host.
    # Everything inside this block is the server&#39;s lifetime.
    async with stdio_server() as (read_stream, write_stream):
        print(&#34;MCP server started&#34;, file=sys.stderr)   # stderr is safe for debug
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options(),
        )
if __name__ == &#34;__main__&#34;:
    asyncio.run(main())
</code></pre><p>💡<strong>Why</strong> <strong>async?</strong> MCP uses async I/O because servers often wait on multiple things at once — the host, network calls, file reads. Python&rsquo;s asyncio lets a single thread handle all of that without blocking. You don&rsquo;t need to understand the internals; just know that every handler must be async def, and use await before any I/O call.</p>
<h3 id="connecting-your-server-to-claudedesktop">Connecting Your Server to Claude Desktop</h3>
<p>Once you’ve written your server.py, plugging it into Claude Desktop takes two minutes. Download Claude Desktop from <strong>claude.ai/download</strong> — it&rsquo;s free and has an MCP client built in. Then locate the config file for your OS:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-06.png"></p>
<p>Register your server by adding it to that file:</p>
<p>CLAUDE_DESKTOP_CONFIG.JSON</p>
<pre tabindex="0"><code>{
  &#34;mcpServers&#34;: {
    &#34;my-first-mcp-server&#34;: {
      &#34;command&#34;: &#34;python&#34;,
      &#34;args&#34;: [
        &#34;/absolute/path/to/my-first-mcp-server/server.py&#34;
      ],
      &#34;env&#34;: {
        &#34;MY_API_KEY&#34;: &#34;sk-...&#34;    // Pass secrets here; read with os.environ in Python
      }
    }
  }
}
</code></pre><blockquote>
<p><strong>Important:</strong> Use the <em>absolute path</em> to your server.py file. No tildes, no relative paths. Get the path with pwd on Mac/Linux or cd on Windows, and paste the full thing in.</p></blockquote>
<p>Fully quit Claude Desktop (not just close the window) and reopen it. Look for the 🔧 tools icon in the chat input — that means MCP is connected. Then just ask Claude naturally: <em>“Tell me a dad joke”</em> or <em>“What tasks does Alice have?”</em> — it figures out on its own when to call your tools. No special syntax needed.</p>
<h3 id="best-practices-aka-mistakes-im-saving-youfrom">Best Practices (a.k.a. Mistakes I’m Saving You From)</h3>
<p><strong>Write excellent descriptions</strong></p>
<blockquote>
<p>The AI reads your tool descriptions to decide when to use them. Vague descriptions = confused AI = bad results. Be specific: what does the tool do, when should it be used, what do the arguments mean?</p></blockquote>
<p><strong>Never</strong> <strong>print() to stdout for debug output</strong></p>
<blockquote>
<p>In stdio transport, stdout is the MCP communication channel. Any stray print() call will corrupt the protocol stream. Always write debug output to stderr: print(&ldquo;debug&rdquo;, file=sys.stderr).</p></blockquote>
<p><strong>Validate your inputs</strong></p>
<blockquote>
<p>The AI sometimes passes unexpected values. Check that required arguments exist. Validate formats. Return helpful error messages rather than cryptic crashes.</p></blockquote>
<p><strong>Keep tools focused</strong></p>
<blockquote>
<p>One tool, one job. Don’t build a do_everything() tool. Small, well-named tools are easier for the AI to reason about and call correctly. Think Unix philosophy: do one thing well.</p></blockquote>
<p><strong>Use environment variables for secrets</strong></p>
<blockquote>
<p>Never hardcode API keys. Pass them via the env key in your claude_desktop_config.json: &ldquo;env&rdquo;: { &ldquo;API_KEY&rdquo;: &ldquo;your-key-here&rdquo; }. Read with os.environ[&ldquo;API_KEY&rdquo;] in your server.</p></blockquote>
<h3 id="the-mcp-ecosystem-you-dont-have-to-build-everything">The MCP Ecosystem: You Don’t Have to Build Everything</h3>
<p>Here’s some great news: the open-source community has already built hundreds of MCP servers you can just plug in and use. Some highlights:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mcp-the-protocol-that-finally-taught-ai-to-stop-being-useless/image-07.png"></p>
<p>Find these and more at <strong>github.com/modelcontextprotocol/servers</strong> — the official repo — and on <strong>mcp.so</strong>, a community directory. There’s a good chance what you need already exists.</p>
<h3 id="wrapping-up-why-thismatters">Wrapping Up: Why This Matters</h3>
<p>MCP isn’t just a neat technical standard. It represents a fundamental shift in what AI assistants actually are.</p>
<p>Before MCP: AI as an <em>oracle</em>. You ask, it tells. Great for knowledge questions. Useless for action.</p>
<p>After MCP: AI as an <em>agent</em>. You describe what you want, it figures out which tools to use, calls them, interprets the results, and gets the job done. The AI becomes an active participant rather than a fancy search engine.</p>
<blockquote>
<p>The future isn’t AI that can describe how to fix your code. It’s AI that fixes your code, runs the tests, creates the PR, and messages your team. MCP is the foundation that makes all of that possible.</p></blockquote>
<p>You’ve now seen the full picture: the history, the architecture, the wire format, and a working server from scratch. The next step is yours. Pick a tool you use daily — a database, an API, a local service — and wrap it in an MCP server. Once you’ve done it once, you’ll find excuses to do it for everything.</p>
<p>Happy building. 🚀</p>
]]></content:encoded></item><item><title>Claude Code: The Complete Guide Nobody Wrote</title><link>https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/</link><pubDate>Tue, 02 Jun 2026 17:21:27 +0000</pubDate><guid>https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-01.png">&lt;/p>
&lt;h3 id="the-agentic-loop-how-claude-actuallythinks">The Agentic Loop (How Claude Actually Thinks)&lt;/h3>
&lt;p>Before anything else, you need to understand what makes Claude Code different from your IDE’s autocomplete. Autocomplete guesses the next word. Claude Code runs a loop — it gathers context, takes action, verifies results, then repeats until the task is done. Think of it less like a text editor feature and more like a developer who works asynchronously while you make coffee.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-01.png"></p>
<h3 id="the-agentic-loop-how-claude-actuallythinks">The Agentic Loop (How Claude Actually Thinks)</h3>
<p>Before anything else, you need to understand what makes Claude Code different from your IDE’s autocomplete. Autocomplete guesses the next word. Claude Code runs a loop — it gathers context, takes action, verifies results, then repeats until the task is done. Think of it less like a text editor feature and more like a developer who works asynchronously while you make coffee.</p>
<p>The loop has three phases that blend together depending on the task:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-02.png"></p>
<p>The loop adapts to what you ask. A question might only hit phase 1. A bug fix cycles through all three phases multiple times. A full feature might chain dozens of tool calls before Claude surfaces for air. You’re part of the loop too — you can interrupt at any point with Ctrl+C and steer it in a different direction.</p>
<blockquote>
<p><strong>THE HONEST SUMMARY</strong></p></blockquote>
<blockquote>
<p>Claude Code is basically a language model with access to your entire computer strapped to it. It can read everything, run anything you could run from a terminal, and it just keeps going until it either finishes the task or you tell it to stop. This is either exhilarating or terrifying depending on how much you trust your test coverage.</p></blockquote>
<h3 id="sessions--how-claude-remembers-ordoesnt">Sessions &amp; How Claude Remembers (Or Doesn’t)</h3>
<p>Here’s the thing about Claude: <strong>every session starts with a completely blank memory.</strong> No recollection of last week’s chat. No memory of that convention you mentioned. Tabula rasa. Every. Single. Time. If you don’t explicitly persist knowledge, it’s gone when the session closes.</p>
<p>This isn’t a bug — it’s how LLMs work. But Claude Code gives you two mechanisms to work around it:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-03.png"></p>
<h4 id="how-a-session-actuallystarts">How a session actually starts</h4>
<p>When you run claude in a directory, here&rsquo;s what happens in the first few seconds before you&rsquo;ve typed anything:</p>
<ol>
<li>**Settings load (all scopes)**Global ~/.claude/settings.json → project .claude/settings.json → local .claude/settings.local.json. Scalar settings use the most specific value; array settings (like permissions) merge across all scopes.</li>
<li><strong>MCP servers connect</strong>All configured MCP servers from .mcp.json and ~/.claude.json start up. Tool schemas are deferred and load on demand. If a server fails to connect, Claude continues without it — it just loses those tools.</li>
<li><strong>CLAUDE.md files load into context</strong>Org-level → user-level (~/.claude/CLAUDE.md) → project-level (./CLAUDE.md) → local (./CLAUDE.local.md). Plus your ~/.claude/MEMORY.md auto memory. All of this costs context window tokens immediately.</li>
<li><strong>Plugins activate</strong>All enabled plugins load their skills, hooks, MCP servers, and settings. Run /reload-plugins to activate changes mid-session.</li>
<li><strong>Session is ready</strong>You see the prompt. Everything above has already happened. Claude now has your project context and is ready to work.</li>
</ol>
<blockquote>
<p><strong>CONTEXT WINDOW BUDGET</strong></p></blockquote>
<blockquote>
<p>Everything loaded at startup costs tokens before you type a single character. A bloated CLAUDE.md, a dozen MCP servers, and a large MEMORY.md can eat a significant chunk of your context window. Run /status to see current token usage. Use /compact when a session gets long.</p></blockquote>
<h3 id="theclaude-directoryevery-folder-explained">The .claude Directory — Every Folder Explained</h3>
<p>The .claude/ folder is Claude Code&rsquo;s configuration home in your project. There&rsquo;s also ~/.claude/ in your home directory for global settings. Here&rsquo;s the complete layout with every file and folder you&rsquo;ll actually encounter:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-04.png"></p>
<blockquote>
<p><strong>COMMIT STRATEGY</strong></p></blockquote>
<blockquote>
<p>Everything marked <strong>committed</strong> should go in Git so your whole team gets the same Claude setup. Everything marked <strong>gitignored</strong> is personal — Claude automatically adds settings.local.json to your global Git ignore. Add CLAUDE.local.md to your project&rsquo;s .gitignore manually.</p></blockquote>
<h3 id="claudemdthe-fullstory">CLAUDE.md — The Full Story</h3>
<p>You’ve heard about CLAUDE.md. Everyone mentions it. But few people explain the <em>entire</em> system behind it, which has four separate scope levels that load in a specific order:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-05.png"></p>
<p>They load in that order — managed first, local last. More specific scopes appear later in the context window, giving them higher effective priority.</p>
<h4 id="what-to-actually-put-in-claudemd">What to actually put in CLAUDE.md</h4>
<p>The golden rule: add something to CLAUDE.md when Claude makes the same mistake a second time. Here’s a battle-tested example for a Spring Boot project:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-06.png"></p>
<blockquote>
<p><strong>SIZE MATTERS</strong></p></blockquote>
<blockquote>
<p>Target under 200 lines per CLAUDE.md. Longer files still load fully but adherence drops. If it’s getting long, split topic-specific rules into .claude/rules/ files and skills for complex procedures — they load conditionally and save your context window.</p></blockquote>
<h4 id="auto-memory-memorymd">Auto Memory (MEMORY.md)</h4>
<p>This is the magical part most people miss. Claude can write its own notes — automatically. When you correct Claude, it optionally remembers that correction for next session in ~/.claude/MEMORY.md. The first 200 lines or 25KB load at the start of every session.</p>
<p>Enable it with /memory in session or toggle it in settings. Then: tell Claude something once, it remembers it forever (until you delete the entry). This is how you get a Claude that feels like it&rsquo;s been on your team for months.</p>
<h3 id="settingsjsonthe-enforcement-layer">settings.json — The Enforcement Layer</h3>
<p>There’s a critical distinction most people miss: <strong>CLAUDE.md is guidance Claude reads.settings.json is configuration Claude Code enforces.</strong> One is a polite suggestion. The other is a locked door.</p>
<p>Settings live in .claude/settings.json (project/team) and .claude/settings.local.json(personal overrides). Here&rsquo;s what you can configure:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-07.png"></p>
<blockquote>
<p><strong>ALLOW VS DENY PRECEDENCE</strong></p></blockquote>
<blockquote>
<p>Deny rules always win over allow rules. If you allow Bash(kubectl *) and deny Bash(kubectl delete *), the delete command is blocked. Patterns support wildcards — Bash(npm test *)matches any command starting with npm test.</p></blockquote>
<h3 id="rulesscoped-instructions-that-load-ondemand">rules/ — Scoped Instructions That Load On Demand</h3>
<p>Once your CLAUDE.md hits 200 lines, start splitting it into .claude/rules/. Rules are Markdown files that can load <em>conditionally</em> based on which files Claude is working with — only when it reads a file that matches the rule&rsquo;s path pattern. This keeps your global context lean while ensuring Claude gets the right rules at the right time.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-08.png"></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-09.png"></p>
<blockquote>
<p><strong>THE REAL POWER</strong></p></blockquote>
<blockquote>
<p>A rule file without paths: frontmatter loads every session like CLAUDE.md. A rule WITH paths:only loads when Claude reads a matching file. This means your Kafka rules don&rsquo;t eat context window during a CLAUDE.md update, and your migration rules only appear when Claude actually opens a migration file.</p></blockquote>
<h3 id="skillsyour-custom-slashcommands">skills/ — Your Custom Slash Commands</h3>
<p>Skills are reusable prompts you or Claude can invoke by name using /skill-name. Think of them as saved workflows — things you do repeatedly that are more than a one-liner but less than a custom tool. You write the instructions once in a SKILL.md, and invoke it whenever you need it.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-10.png"></p>
<p>Now you type /db-migrate and Claude walks through all seven steps. No more explaining the process from scratch every time. Skills also accept arguments via $ARGUMENTS:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-11.png"></p>
<p>Usage: /review-pr 127 — Claude reviews MR #127 with your exact conventions baked in.</p>
<blockquote>
<p>SKILLS VS COMMANDS</p></blockquote>
<blockquote>
<p>You may see references to .claude/commands/ in older docs — this is the same mechanism as skills. For new workflows, use skills/ instead: same /name invocation, plus you can bundle supporting files alongside the SKILL.md.</p></blockquote>
<h3 id="hooksthe-automation-engine">Hooks — The Automation Engine</h3>
<p>Hooks are shell commands that fire automatically at specific points in Claude’s lifecycle. They’re deterministic — they <em>always</em> run when triggered, regardless of what Claude decides. This is the difference between “please format the file” (Claude might forget) and “PostToolUse hook runs Spotless” (it definitely happens).</p>
<h4 id="available-hookevents">Available hook events</h4>
<p><strong>PreToolUse</strong><em>Fires before any tool call. Can block execution (exit 2). Use to protect files or validate commands.</em></p>
<p><strong>PostToolUse</strong><em>Fires after a tool completes. Perfect for auto-formatting files after Claude edits them.</em></p>
<p><strong>Notification</strong><em>Fires when Claude needs your input. Desktop notification so you don’t stare at the terminal.</em></p>
<p><strong>SessionStart</strong><em>Fires when a session begins. Inject additional context, set environment, run setup scripts.</em></p>
<p><strong>PreCompact</strong><em>Fires before conversation compaction. Re-inject context that would otherwise be lost.</em></p>
<p><strong>Stop</strong><em>Fires when Claude finishes a response. Useful for post-response validation or logging.</em></p>
<h3 id="practical-examples-for-a-javaspring-bootproject">Practical examples for a Java/Spring Boot project</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-12.png"></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-13.png"></p>
<blockquote>
<p><strong>EXIT CODES MATTER</strong></p></blockquote>
<blockquote>
<p>exit 0 = allow. exit 1 = warn Claude but let it continue. exit 2 = hard block — Claude gets told why and has to try something else. It&rsquo;s the digital equivalent of a bouncer who also explains the dress code.</p></blockquote>
<h3 id="mcpconnecting-claude-to-theworld">MCP — Connecting Claude to the World</h3>
<p>Model Context Protocol (MCP) is an open standard for connecting AI to external tools. Without MCP, Claude can only see your filesystem and run terminal commands. With MCP, Claude can query your Jira board, check Sentry errors, read Slack messages, query your PostgreSQL database, and more — all without you copying and pasting anything.</p>
<blockquote>
<p><strong>THE MENTAL MODEL</strong></p></blockquote>
<blockquote>
<p>MCP is how Claude gets a login to your tools. Without it: “here’s a pasted error from Sentry.” With it: “Claude, check what errors happened in prod in the last hour and correlate them with my recent commits.”</p></blockquote>
<h4 id="three-types-of-mcpservers">Three types of MCP servers</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-14.png"></p>
<h4 id="project-scoped-mcp-mcpjson">Project-scoped MCP (.mcp.json)</h4>
<p>Put your team’s MCP servers in .mcp.json at the project root. This file gets committed to Git so everyone on the team gets the same tools:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-15.png"></p>
<h4 id="user-scoped-mcp-personal-tools">User-scoped MCP (personal tools)</h4>
<p>For tools only you need (not your team), use the &ndash;scope user flag. This writes to ~/.claude.json instead of .mcp.json:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-16.png"></p>
<blockquote>
<p><strong>CHANNELS: MCP THAT PUSHES TO CLAUDE</strong></p></blockquote>
<blockquote>
<p>Normal MCP is pull — Claude queries a server when it needs something. Channels flip this: an MCP server <em>pushes</em> messages into your running session. This means Claude can react to CI results, monitoring alerts, or Slack messages while you’re doing something else. Enable with &ndash;channelsflag on startup.</p></blockquote>
<h3 id="pluginspackage--share-everything">Plugins — Package &amp; Share Everything</h3>
<p>Everything you’ve built — skills, hooks, MCP servers, rules — can be packaged into a <strong>plugin</strong> and shared with your team, your org, or the whole world. Plugins are just directories with a .claude-plugin/plugin.json manifest. They&rsquo;re the difference between &ldquo;config that lives in one project&rdquo; and &ldquo;config that lives everywhere.&rdquo;</p>
<h4 id="standalone-config-vsplugins">Standalone config vs plugins</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-17.png"></p>
<h4 id="plugin-structure">Plugin structure</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-18.png"></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-19.png"></p>
<h4 id="installing-aplugin">Installing a plugin</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-20.png"></p>
<blockquote>
<p><strong>THE REAL VALUE</strong></p></blockquote>
<blockquote>
<p>Your platform team builds a plugin once with all the Spring Boot conventions, Kafka rules, code review skills, and MCP servers your org uses. Every developer installs it once. Every project gets the same setup. No more “did you add the Flyway rules to your CLAUDE.md?” Slack messages.</p></blockquote>
<h3 id="subagentsclaude-spawningclaude">Subagents — Claude Spawning Claude</h3>
<p>This is the part where things get genuinely science-fiction. Subagents let Claude spin up <em>other Claude instances</em> to handle subtasks in parallel or in isolated contexts. The main Claude orchestrates; the subagents execute. Think distributed computing, but the workers are also AI and can also spawn more workers.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-the-complete-guide-nobody-wrote/image-21.png"></p>
<p>Subagents can maintain their own MEMORY.md too. Imagine a &ldquo;database specialist&rdquo; subagent that remembers every schema change it&rsquo;s ever helped with. That&rsquo;s the direction this is heading.</p>
<blockquote>
<p><strong>WHEN TO USE SUBAGENTS</strong></p></blockquote>
<blockquote>
<p>For most day-to-day tasks, you don’t need subagents. They’re most useful when you have truly parallel workstreams (implement feature A and feature B simultaneously without conflicts), when you need a clean context for a specific task, or when you’re building automation pipelines that run without human interaction.</p></blockquote>
<h3 id="complete-quick-reference">Complete Quick Reference</h3>
<ul>
<li><strong>.claude/settings.json</strong> — permissions + hooks + model config. Enforced, not just read.</li>
<li><strong>CLAUDE.md</strong> — persistent instructions. Team version in ./CLAUDE.md, personal in ~/.claude/CLAUDE.md.</li>
<li><strong>MEMORY.md</strong> — auto-written by Claude. Persists learnings across sessions (200 lines / 25KB limit).</li>
<li><strong>.claude/rules/</strong> — conditional instruction files. Use paths: frontmatter to load only for relevant files.</li>
<li><strong>.claude/skills/</strong> — custom /commands. Each skill = a folder with SKILL.md. Accepts $ARGUMENTS.</li>
<li><strong>hooks</strong> — deterministic automation. PreToolUse (block/allow), PostToolUse (format), Notification (alert).</li>
<li><strong>.mcp.json</strong> — team MCP servers (committed). ~/.claude.json for personal servers.</li>
<li><strong>Plugins</strong> — packaged, shareable bundles of skills + hooks + MCP + settings. Install with /plugin install.</li>
<li><strong>Subagents</strong> — parallel Claude instances for complex multi-track workflows. Three isolation levels.</li>
<li><strong>/init</strong> — always run first in a new project. Generates CLAUDE.md automatically.</li>
<li><strong>/compact</strong> — compresses long sessions. <strong>/clear</strong> = fresh start (CLAUDE.md reloads).</li>
<li><strong>/mcp</strong> — shows server status + tool count. <strong>/hooks</strong> — shows configured hooks. <strong>/memory</strong> — edit CLAUDE.md.</li>
</ul>
]]></content:encoded></item><item><title>Claude Code: A Beginner’s Guide That Doesn’t Put You to Sleep</title><link>https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/</link><pubDate>Wed, 27 May 2026 17:33:07 +0000</pubDate><guid>https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/</guid><description>&lt;p>For developers who want an AI in their terminal and still have no idea what CLAUDE.md is. You’ve come to the right place.&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/image-01.png">&lt;/p>
&lt;h3 id="-what-even-is-claudecode">🤔 What Even IS Claude Code?&lt;/h3>
&lt;p>Okay so you’ve heard of GitHub Copilot, right? Claude Code is like that, except instead of whispering suggestions into your IDE like a shy intern, it’s a &lt;strong>full-blown agentic assistant that lives in your terminal&lt;/strong> and acts like a senior developer who has had three espressos and is ready to ship.&lt;/p></description><content:encoded><![CDATA[<p>For developers who want an AI in their terminal and still have no idea what CLAUDE.md is. You’ve come to the right place.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/image-01.png"></p>
<h3 id="-what-even-is-claudecode">🤔 What Even IS Claude Code?</h3>
<p>Okay so you’ve heard of GitHub Copilot, right? Claude Code is like that, except instead of whispering suggestions into your IDE like a shy intern, it’s a <strong>full-blown agentic assistant that lives in your terminal</strong> and acts like a senior developer who has had three espressos and is ready to ship.</p>
<p>It can read your <em>entire codebase</em>, plan multi-step changes, edit multiple files, run your tests, and even commit code. All from the command line. No IDE plugin. No browser tab. Just you, your terminal, and your new AI best friend who never judges you for that 200-line method you wrote at 2am.</p>
<blockquote>
<p>🧠 Fun fact</p></blockquote>
<blockquote>
<p>Claude Code holds up to <strong>200,000 tokens</strong> of context in memory. That’s your entire project, your architecture docs, and probably your unfinished README — all at once. Copilot gets about 8,000. Do the math.</p></blockquote>
<p>The key difference from other AI tools: Claude Code is <strong>agentic</strong>. It doesn’t just suggest — it <em>does</em>. Ask it to “add authentication to the user service” and it’ll actually read your code, figure out what you’re using, and make it happen. It’s the difference between a GPS giving you directions and a GPS that also drives the car.</p>
<blockquote>
<p>“I used to Google ‘how to reverse a string in Java’ every single time. Now Claude just does it for me. My Google history has never been cleaner.”</p></blockquote>
<h3 id="-prerequisites-yes-you-need-topay">💳 Prerequisites (Yes, You Need to Pay)</h3>
<p>Sorry, no free lunch here. But the good news is the cheapest plan already includes everything you need to get started:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/image-02.png"></p>
<blockquote>
<p>⚠️ Heads up</p></blockquote>
<blockquote>
<p>If you run a lot of Claude Code sessions simultaneously, you’ll burn through your rate limits faster than a frontend dev burning through <code>console.log()</code> statements. Claude Max ($100–200/month) exists for that use case.</p></blockquote>
<h3 id="-installation-the-easy-part-wepromise">⚙️ Installation (The Easy Part, We Promise)</h3>
<p>One line. That’s it. No npm, no Homebrew tap, no reading a 47-step Medium article. Just paste this and go make a coffee while it installs:</p>
<h4 id="macos-linux">macOS / Linux</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># This one line is doing more work than you did all morning</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://claude.ai/install.sh <span class="p">|</span> bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Make sure it worked</span>
</span></span><span class="line"><span class="cl">claude --version
</span></span></code></pre></div><h4 id="windows-powershell">Windows (PowerShell)</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># For our Windows friends. No judgment. (Okay, maybe a little.)</span>
</span></span><span class="line"><span class="cl">irm https://claude.ai/install.ps1 <span class="p">|</span> iex
</span></span></code></pre></div><h3 id="homebrew-macos-if-yourefancy">Homebrew (macOS, if you’re fancy)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="nt">brew</span> <span class="nt">install</span> <span class="nt">--cask</span> <span class="nt">claude-code</span>
</span></span></code></pre></div><blockquote>
<p>🚫 Don’t do this</p></blockquote>
<blockquote>
<p>The old way — <code>npm install -g @anthropic-ai/claude-code</code> — is <strong>officially deprecated</strong>. If you used it before, run <code>claude install</code> to migrate. Your npm-installed past is a mistake you can fix.</p></blockquote>
<h3 id="first-launch-authenticate">First launch: authenticate</h3>
<p>Run <code>claude</code> in any directory. A browser window opens, you log in with your Claude account, and boom — you&rsquo;re in. If you&rsquo;re behind a corporate firewall that blocks OAuth callbacks (classic), use an API key via the <code>ANTHROPIC_API_KEY</code> environment variable instead.</p>
<blockquote>
<p>💡 Pro tip</p></blockquote>
<blockquote>
<p>If it fails to authenticate, check your <code>.claudeignore</code> isn&rsquo;t too aggressive. Actually wait — you don&rsquo;t have one yet. Never mind, you&rsquo;re fine. Keep going.</p></blockquote>
<h3 id="-your-first-session-dontpanic">🐣 Your First Session (Don’t Panic)</h3>
<p>Navigate to your project and type <code>claude</code>. You&rsquo;ll see the Claude Code prompt. Here&rsquo;s the crucial part: <strong>don&rsquo;t just immediately say &ldquo;build me a feature.&rdquo;</strong> That&rsquo;s like hiring a new developer and skipping the entire onboarding process. Claude needs context. Give it a tour first.</p>
<ol>
<li><strong>Run</strong> <code>/init</code><strong>— Let Claude scan your project</strong><em>This generates a starter</em> <code>CLAUDE.md</code> <em>file. Claude reads your directory structure, sniffs out your build system and test framework, and writes a first draft. It&rsquo;s like Claude doing its own onboarding. Adorable and useful.</em></li>
<li><strong>Ask Claude to explain things before touching them</strong><em>Try: “Explain the architecture of this project.” or “How does authentication work here?” Claude builds a mental model of your code. You’ll get better results when it actually understands what it’s working with.</em></li>
<li><strong>Enable Plan Mode with Shift+Tab before any change</strong><em>Claude will describe exactly what it intends to do before touching a single file. Think of it as asking a surgeon to show you the plan before they start cutting. Highly recommended.</em></li>
<li><strong>Review → Approve → Commit → Repeat</strong><em>Read the diff. Run your tests. Commit small. If Claude did something weird, a small commit is easy to revert. “I deleted the database” is much funnier as a story than an actual incident.</em></li>
</ol>
<h3 id="-the-claudemd-file-where-the-magiclives">✨ The CLAUDE.md File (Where the Magic Lives)</h3>
<p>This is the most important thing in this whole guide. Read it twice.</p>
<p><code>CLAUDE.md</code> is a Markdown file you put in your project root. Claude reads it <strong>automatically at the start of every single session</strong>. It&rsquo;s like a briefing document for your AI colleague — except unlike your actual team&rsquo;s onboarding docs, people actually read this one.</p>
<p>Without it, Claude starts every session with zero context about your project, your conventions, your stack, your rules. <em>With</em> it, Claude starts every session knowing exactly how you work.</p>
<blockquote>
<p><strong>🎯 The one-sentence summary:</strong> CLAUDE.md is your project’s brain dump, and Claude will actually remember and use everything you put in it — unlike that Wiki your team hasn’t touched since 2022.</p></blockquote>
<h4 id="a-real-claudemd-for-a-spring-bootproject">A real CLAUDE.md for a Spring Boot project</h4>
<p>Here’s a solid example for a Java/Spring Boot/PostgreSQL stack:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># CLAUDE.MD
</span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gu">## Project
</span></span></span><span class="line"><span class="cl"><span class="gu"></span>REST API for customer order management. Java 17, Spring Boot 3,
</span></span><span class="line"><span class="cl">PostgreSQL 15, deployed on AWS EKS via Helm + ArgoCD.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Commands
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="k">-</span> Build:   <span class="sb">`./mvnw clean package -DskipTests`</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> Test:    <span class="sb">`./mvnw test`</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> Run:     <span class="sb">`./mvnw spring-boot:run`</span>
</span></span><span class="line"><span class="cl"><span class="k">-</span> Lint:    <span class="sb">`./mvnw checkstyle:check`</span>
</span></span><span class="line"><span class="cl"><span class="gu">## Architecture
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="k">-</span> Layered: Controller → Service → Repository (Spring Data JPA)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Async events via Spring ApplicationEventPublisher
</span></span><span class="line"><span class="cl"><span class="k">-</span> DB migrations: Flyway (src/main/resources/db/migration)
</span></span><span class="line"><span class="cl"><span class="k">-</span> All config via environment variables. No hardcoded secrets. Ever.
</span></span><span class="line"><span class="cl"><span class="gu">## Conventions
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="k">-</span> Constructor injection only. <span class="ni">@Autowired</span> on fields is a war crime.
</span></span><span class="line"><span class="cl"><span class="k">-</span> All endpoints return ResponseEntity with standard error envelope
</span></span><span class="line"><span class="cl"><span class="k">-</span> Unit tests: JUnit 5 + Mockito. Integration tests: <span class="ni">@SpringBootTest</span> + Testcontainers
</span></span><span class="line"><span class="cl"><span class="k">-</span> Commits: imperative mood, max 72 chars (&#34;Add order validation&#34;, not &#34;Added&#34;)
</span></span><span class="line"><span class="cl"><span class="gu">## Hard Rules
</span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="k">-</span> Never modify existing Flyway migration files. Never.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Never commit .env or application-local.properties
</span></span><span class="line"><span class="cl"><span class="k">-</span> No wildcard imports
</span></span></code></pre></div><blockquote>
<p>💡 Pro tip</p></blockquote>
<blockquote>
<p><strong>Commit your CLAUDE.md to Git.</strong> Every teammate then gets the same Claude setup automatically. No more Claude giving one dev constructor injection advice and another dev <code>@Autowired</code> advice in the same codebase.</p></blockquote>
<h3 id="-claudemd-best-practices-what-actuallyworks">📝 CLAUDE.md Best Practices (What Actually Works)</h3>
<h4 id="keep-it-under-100lines">Keep it under 100 lines</h4>
<p>Seriously. For every line you add, ask: <em>“Would removing this cause Claude to make a mistake?”</em> If no, delete it. A fat CLAUDE.md with 500 lines of noise is worse than a lean one with 60 lines of signal. Less is more. (Yes, the same rule applies to your commit messages.)</p>
<h4 id="use-the-why--what--how-structure">Use the WHY / WHAT / HOW structure</h4>
<ul>
<li><strong>WHY</strong> — One paragraph on what the project exists to do. Domain context matters. Claude giving advice on an e-commerce API vs a healthcare API should feel different.</li>
<li><strong>WHAT</strong> — Tech stack, key dependencies, deployment target. Be specific about versions.</li>
<li><strong>HOW</strong> — Exact commands. Exact conventions. Your team’s particular quirks. The stuff Claude can’t infer from the code alone.</li>
</ul>
<h4 id="-good-things-to-put-in-claudemd">✅ Good things to put in CLAUDE.md</h4>
<ul>
<li>Build, test, lint, run commands — saves Claude from guessing and running the wrong thing</li>
<li>Architecture summary — layers, patterns, where important things live</li>
<li>Non-obvious rules — things that would surprise a new dev (“never modify migration files”)</li>
<li>Testing philosophy — unit vs integration, which frameworks, naming conventions</li>
<li>Things that have burned you before — sprinkle your trauma in there</li>
</ul>
<h4 id="-things-not-to-put-in-claudemd">🚫 Things NOT to put in CLAUDE.md</h4>
<ul>
<li>Code style rules your linter already enforces — don’t tell Claude twice what Checkstyle already screams about</li>
<li>Secrets, passwords, API keys — you know this, but we’re saying it anyway</li>
<li>Entire config files pasted in — link or summarise, don’t paste 200 lines of XML</li>
<li>Your personal frustrations about legacy code — Claude doesn’t need your trauma dump</li>
</ul>
<h4 id="bonus-sub-directory-claudemd-for-bigprojects">Bonus: Sub-directory CLAUDE.md for big projects</h4>
<p>Monorepo? Complex project with distinct modules? Put a <code>CLAUDE.md</code> inside subdirectories too. Claude reads and merges them — the root file is the global rules, the subdirectory file adds module-specific context. Like a company handbook + a team-specific playbook.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># SRC/NOTIFICATION-SERVICE/CLAUDE.MD</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Notification Service</span>
</span></span><span class="line"><span class="cl">Handles email + push via AWS SES and SNS.
</span></span><span class="line"><span class="cl">Templates: src/main/resources/templates <span class="o">(</span>Thymeleaf<span class="o">)</span>.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Module Rules</span>
</span></span><span class="line"><span class="cl">- All outbound calls MUST go through NotificationGateway
</span></span><span class="line"><span class="cl">- Do not call SES/SNS directly from service classes
</span></span><span class="line"><span class="cl">- Retry logic: Spring Retry handles it. Don<span class="err">&#39;</span>t add your own loops.
</span></span></code></pre></div><h3 id="-theclaudeignore-file">🙈 The .claudeignore File</h3>
<p>Like <code>.gitignore</code> but for Claude. It tells Claude which files to skip so it doesn&rsquo;t waste context on your <code>target/</code> folder and doesn&rsquo;t accidentally read your <code>.env</code> file out loud during pair programming.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .CLAUDEIGNORE# Build output — Claude doesn&#39;t need to read your compiled bytecode</span>
</span></span><span class="line"><span class="cl">target/
</span></span><span class="line"><span class="cl">build/
</span></span><span class="line"><span class="cl">dist/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Secrets - for the love of all that is holy</span>
</span></span><span class="line"><span class="cl">.env
</span></span><span class="line"><span class="cl">.env.local
</span></span><span class="line"><span class="cl">application-local.properties
</span></span><span class="line"><span class="cl">*.pem
</span></span><span class="line"><span class="cl">*.key
</span></span><span class="line"><span class="cl"><span class="c1"># Dependencies - it knows what Spring Boot is, trust it</span>
</span></span><span class="line"><span class="cl">node_modules/
</span></span><span class="line"><span class="cl">.gradle/
</span></span><span class="line"><span class="cl"><span class="c1"># Generated code (if you don&#39;t want Claude accidentally &#34;fixing&#34; it)</span>
</span></span><span class="line"><span class="cl">src/generated/
</span></span></code></pre></div><blockquote>
<p>🚫 Classic mistake</p></blockquote>
<blockquote>
<p>Adding something too broad like <code>*.java</code> to <code>.claudeignore</code>. Now Claude can&rsquo;t read any of your source files. It will be very polite about it but also completely useless. Be specific with your ignore patterns.</p></blockquote>
<h3 id="-plan-mode-look-before-youleap">🛡️ Plan Mode (Look Before You Leap)</h3>
<p>Plan Mode is what separates “Claude deleted half my project” stories from “Claude is amazing” stories. Before making any changes, Claude tells you <em>exactly</em> what it plans to do: which files it’ll touch, what changes it’ll make, and why. You say yes or no. Then it acts.</p>
<blockquote>
<p><strong>⌨️ Just press</strong> <code>Shift + Tab</code> <strong>to toggle it on.</strong> That&rsquo;s it. Best keyboard shortcut you&rsquo;ll use today.</p></blockquote>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/image-03.png"></p>
<blockquote>
<p>😌 Wisdom</p></blockquote>
<blockquote>
<p>As a beginner, use Plan Mode for <em>everything</em>. As you build trust in Claude’s output quality (and your own CLAUDE.md), you can skip it for small, well-understood tasks. Think of it like asking your junior dev to write tests first vs. just letting your senior dev rip.</p></blockquote>
<h3 id="-the-holy-workflow-explore--plan--implement-commit">🔄 The Holy Workflow: Explore → Plan → Implement → Commit</h3>
<p>This is Anthropic’s official recommended workflow. It sounds obvious but most beginners skip the first two steps, and that’s why they end up on Reddit asking why Claude rewrote their entire service layer.</p>
<ol>
<li><strong>🔍 Explore — read before you write</strong><em>Ask Claude to explain the relevant parts of your code before asking it to change anything. “How does order processing work in this project?” Claude builds a proper mental model. Future changes will actually fit your architecture.</em></li>
<li><strong>📋 Plan — agree on the approach</strong><em>Enable Plan Mode. Describe what you want. Push back on the plan if something seems wrong. This is the cheapest place to catch misunderstandings — before any code is written.</em></li>
<li><strong>⚙️ Implement — let Claude cook</strong><em>With the plan approved, Claude implements. Watch the output. If something looks off, hit</em> <code>Ctrl+C</code><em>. Don&rsquo;t let it finish a 15-file change that you already know is wrong by file 3.</em></li>
<li><strong>✅ Commit — small and often</strong><em>Review the diff. Run tests. Commit. Small commits are your insurance policy. “Revert one small commit” is a two-second job. “Unravel three hours of AI-generated changes” is a Thursday afternoon you’ll never get back.</em></li>
</ol>
<p>“I skipped the Explore step, asked Claude to ‘refactor auth’, and watched it replace my entire session management layer in 45 seconds. It was beautiful and terrifying in equal measure.”</p>
<h3 id="-commands-cheatsheet">⌨️ Commands Cheat Sheet</h3>
<p>Type these inside an active Claude Code session (the ones starting with <code>/</code>):</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/claude-code-a-beginners-guide-that-doesn-t-put-you-to-sleep/image-04.png"></p>
<h3 id="cicd--scripting-mode">CI/CD / Scripting mode</h3>
<p>Claude Code has a <code>--print</code> flag for non-interactive use. Perfect for Jenkins pipelines, GitLab CI jobs, or any situation where you want Claude to run and exit without waiting for your input:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Run a single command and exit — great for pipelines</span>
</span></span><span class="line"><span class="cl">claude --print <span class="s2">&#34;Summarise changes in the last 5 commits&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Works great in your Jenkinsfile or .gitlab-ci.yml</span>
</span></span><span class="line"><span class="cl">claude --print <span class="s2">&#34;Check this changeset for obvious bugs&#34;</span> --no-color
</span></span></code></pre></div><blockquote>
<p>💡 Bonus tip</p></blockquote>
<blockquote>
<p>The <code>/clear</code> command is your friend. When a conversation has gone on too long and Claude starts giving weird answers, clear it. CLAUDE.md re-loads automatically, so you lose the conversation but not the context. Fresh start, same configuration.</p></blockquote>
<h3 id="-the-im-late-to-a-meetingsummary">🎒 The “I’m Late to a Meeting” Summary</h3>
<ul>
<li>Install with one <code>curl</code> command — no Node.js, no npm, no fuss</li>
<li>You need a paid plan — Claude Pro at $20/mo is enough to start</li>
<li>Run <code>/init</code> in every new project. Do it now. Do it always.</li>
<li>CLAUDE.md = Claude’s briefing doc. 50–100 lines, WHY/WHAT/HOW structure</li>
<li>Commit CLAUDE.md to Git — the whole team benefits, zero extra effort</li>
<li>Always use Plan Mode (<code>Shift+Tab</code>) before non-trivial changes</li>
<li>Workflow: Explore → Plan → Implement → Commit. In that order. Always.</li>
<li>Use .claudeignore to keep secrets and build junk out of Claude’s sight</li>
<li>Keep commits small — your future self will thank you</li>
<li>Update your CLAUDE.md as your project grows — stale instructions cause chaos</li>
</ul>
]]></content:encoded></item><item><title>5 Spring Boot Mistakes I See in Every Codebase</title><link>https://sukachvitally.github.io/posts/5-spring-boot-mistakes-i-see-in-every-codebase/</link><pubDate>Sun, 24 May 2026 14:35:12 +0000</pubDate><guid>https://sukachvitally.github.io/posts/5-spring-boot-mistakes-i-see-in-every-codebase/</guid><description>&lt;p>&lt;em>And why senior developers keep making them&lt;/em>&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/5-spring-boot-mistakes-i-see-in-every-codebase/image-01.png">&lt;/p>
&lt;p>I’ve reviewed a lot of Spring Boot codebases. Startups burning through their runway trying to scale. Enterprises with ten-year-old monoliths being carved into microservices. Teams of three and teams of fifty. Different industries, different sizes, different levels of experience.&lt;/p>
&lt;p>The mistakes are almost always the same.&lt;/p>
&lt;p>That’s not an insult — it’s actually the interesting part. These aren’t beginner errors. Most of the teams I work with are smart, experienced engineers who made reasonable decisions under pressure. The problem is that Spring Boot is so productive, so frictionless, that bad patterns compound quietly. You ship fast. The tests pass. Production looks fine. And then six months later you’re staring at a 2,000-line controller and wondering how it got there.&lt;/p></description><content:encoded><![CDATA[<p><em>And why senior developers keep making them</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/5-spring-boot-mistakes-i-see-in-every-codebase/image-01.png"></p>
<p>I’ve reviewed a lot of Spring Boot codebases. Startups burning through their runway trying to scale. Enterprises with ten-year-old monoliths being carved into microservices. Teams of three and teams of fifty. Different industries, different sizes, different levels of experience.</p>
<p>The mistakes are almost always the same.</p>
<p>That’s not an insult — it’s actually the interesting part. These aren’t beginner errors. Most of the teams I work with are smart, experienced engineers who made reasonable decisions under pressure. The problem is that Spring Boot is so productive, so frictionless, that bad patterns compound quietly. You ship fast. The tests pass. Production looks fine. And then six months later you’re staring at a 2,000-line controller and wondering how it got there.</p>
<p>This article is about the five patterns I see most often, why they happen, and — more importantly — how to fix them without rewriting everything from scratch.</p>
<h3 id="mistake-1-putting-business-logic-in-controllers">Mistake 1: Putting business logic in controllers</h3>
<p>This is the most common one by a significant margin. It usually starts innocently: a small validation check here, a conditional there, a bit of data transformation before calling a service. Before long, your <code>@RestController</code> is making decisions it has no business making.</p>
<p>Here’s what it looks like in the wild:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@PostMapping</span><span class="p">(</span><span class="s">&#34;/orders&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">OrderResponse</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">createOrder</span><span class="p">(</span><span class="nd">@RequestBody</span><span class="w"> </span><span class="n">OrderRequest</span><span class="w"> </span><span class="n">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getItems</span><span class="p">().</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">badRequest</span><span class="p">().</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">BigDecimal</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">request</span><span class="p">.</span><span class="na">getItems</span><span class="p">().</span><span class="na">stream</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="na">map</span><span class="p">(</span><span class="n">item</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">item</span><span class="p">.</span><span class="na">getPrice</span><span class="p">().</span><span class="na">multiply</span><span class="p">(</span><span class="n">BigDecimal</span><span class="p">.</span><span class="na">valueOf</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="na">getQuantity</span><span class="p">())))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="na">reduce</span><span class="p">(</span><span class="n">BigDecimal</span><span class="p">.</span><span class="na">ZERO</span><span class="p">,</span><span class="w"> </span><span class="n">BigDecimal</span><span class="p">::</span><span class="n">add</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">total</span><span class="p">.</span><span class="na">compareTo</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">BigDecimal</span><span class="p">(</span><span class="s">&#34;10000&#34;</span><span class="p">))</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">0</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="o">!</span><span class="n">userService</span><span class="p">.</span><span class="na">isPremium</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="na">getUserId</span><span class="p">()))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">HttpStatus</span><span class="p">.</span><span class="na">FORBIDDEN</span><span class="p">).</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// ... and it keeps going</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The controller now knows about pricing rules, user tiers, and order validation. None of that belongs here.</p>
<p><strong>Why it happens:</strong> Controllers are the entry point, so they feel like the natural place to “handle” things. Also, injecting a service and calling one method feels like overkill for “just a quick check.”</p>
<p><strong>Why it matters:</strong> Business logic in controllers can’t be unit tested without spinning up the web layer. It can’t be reused from a message consumer, a scheduled job, or a CLI command. It’s invisible to anyone who looks at your service layer expecting to understand the domain.</p>
<p><strong>The fix:</strong> Your controller should do exactly three things — deserialise the request, call a service method, and serialise the response. Every conditional, every calculation, every rule goes into a service or domain object. If your controller has an <code>if</code>statement that isn&rsquo;t about HTTP concerns, move it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@PostMapping</span><span class="p">(</span><span class="s">&#34;/orders&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">OrderResponse</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">createOrder</span><span class="p">(</span><span class="nd">@RequestBody</span><span class="w"> </span><span class="n">OrderRequest</span><span class="w"> </span><span class="n">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">OrderResponse</span><span class="w"> </span><span class="n">order</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">orderService</span><span class="p">.</span><span class="na">createOrder</span><span class="p">(</span><span class="n">request</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">ok</span><span class="p">(</span><span class="n">order</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Boring. Perfect.</p>
<h3 id="mistake-2-ignoring-transaction-boundaries">Mistake 2: Ignoring transaction boundaries</h3>
<p>Transaction management is one of those topics where a little knowledge is genuinely dangerous. Most developers know <code>@Transactional</code> exists. Fewer understand where to put it, what it actually does, and — critically — what it <em>doesn&rsquo;t</em> do.</p>
<p>The most painful version of this mistake looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">PaymentService</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Transactional</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">processPayment</span><span class="p">(</span><span class="n">Long</span><span class="w"> </span><span class="n">orderId</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Order</span><span class="w"> </span><span class="n">order</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">orderRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">orderId</span><span class="p">).</span><span class="na">orElseThrow</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">order</span><span class="p">.</span><span class="na">setStatus</span><span class="p">(</span><span class="n">OrderStatus</span><span class="p">.</span><span class="na">PAID</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">orderRepository</span><span class="p">.</span><span class="na">save</span><span class="p">(</span><span class="n">order</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// This call is to an external HTTP service.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// If it fails, the order status update is rolled back.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// If it succeeds but the next line throws, you&#39;ve charged</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// the customer but their order is still PENDING.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">paymentGateway</span><span class="p">.</span><span class="na">charge</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="na">getAmount</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">inventoryService</span><span class="p">.</span><span class="na">reserve</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="na">getItems</span><span class="p">());</span><span class="w"> </span><span class="c1">// can throw</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The transaction boundary wraps an external HTTP call and a separate service call. The atomicity guarantee you think you have doesn’t exist across those boundaries.</p>
<p><strong>Why it happens:</strong> <code>@Transactional</code> feels like a safety net you throw over anything that looks important. It&rsquo;s easy to apply and invisible when it&rsquo;s wrong.</p>
<p><strong>Why it matters:</strong> Inconsistent data is the hardest class of bug to diagnose and fix in production. By the time you notice, the damage is already done.</p>
<p><strong>The fix:</strong> Be deliberate about what your transaction actually protects. Keep transactions short, keep them within a single data source, and handle external calls outside the transactional boundary. Consider the outbox pattern for operations that need to be reliably paired with an external call. And always ask: “if this line throws, what state am I leaving the database in?”</p>
<p>Also watch out for the self-invocation trap — calling a <code>@Transactional</code> method from within the same class bypasses the proxy and silently does nothing. This one has burned almost every team I&rsquo;ve worked with.</p>
<h3 id="mistake-3-overusing-autowired-everywhere">Mistake 3: Overusing <code>@Autowired</code> everywhere</h3>
<p>Field injection is convenient. It’s also the option that makes your code harder to test, harder to reason about, and full of hidden dependencies you only discover at runtime.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">ReportService</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">UserRepository</span><span class="w"> </span><span class="n">userRepository</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">OrderRepository</span><span class="w"> </span><span class="n">orderRepository</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">EmailService</span><span class="w"> </span><span class="n">emailService</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">PdfGenerator</span><span class="w"> </span><span class="n">pdfGenerator</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">AuditService</span><span class="w"> </span><span class="n">auditService</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Five dependencies injected invisibly. You can’t instantiate this class in a test without a Spring context, or without reaching for <code>@MockBean</code> and a full application bootstrap. You also can&rsquo;t see — at a glance — that this class might be doing too much.</p>
<p><strong>Why it happens:</strong> Field injection is fewer lines of code and Spring supports it, so it feels fine. Early tutorials often show it this way and the habit sticks.</p>
<p><strong>Why it matters:</strong> Constructor injection makes dependencies explicit and visible. It enables plain unit tests without a Spring context. It makes circular dependencies a compile-time error rather than a runtime surprise. And when a class has eight constructor parameters, that’s a signal — a service with eight dependencies probably has too many responsibilities.</p>
<p><strong>The fix:</strong> Use constructor injection. Always. Since Spring 4.3 you don’t even need <code>@Autowired</code> on a single-constructor class. If you&rsquo;re using Lombok, <code>@RequiredArgsConstructor</code> on <code>final</code> fields gives you this for free.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Service</span>
</span></span><span class="line"><span class="cl"><span class="nd">@RequiredArgsConstructor</span>
</span></span><span class="line"><span class="cl"><span class="k">public</span> <span class="k">class</span> <span class="nc">ReportService</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">final</span> <span class="n">UserRepository</span> <span class="n">userRepository</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">final</span> <span class="n">OrderRepository</span> <span class="n">orderRepository</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">final</span> <span class="n">EmailService</span> <span class="n">emailService</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="o">..</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>If the constructor starts to look unwieldy, take that as the feedback it is.</p>
<h3 id="mistake-4-no-proper-exception-handlingstrategy">Mistake 4: No proper exception handling strategy</h3>
<p>Every codebase has exception handling. Very few have an <em>exception handling strategy</em>. The difference shows up as a patchwork of try-catch blocks scattered across service methods, inconsistent error responses from your API, and stack traces leaking into production responses.</p>
<p>Common signs you don’t have a strategy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// In one service:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">userRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">id</span><span class="p">).</span><span class="na">orElseThrow</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">Exception</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">log</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="s">&#34;Error&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">e</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span><span class="w"> </span><span class="c1">// 🚩 silent failure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// In another:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">user</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">RuntimeException</span><span class="p">(</span><span class="s">&#34;User not found&#34;</span><span class="p">);</span><span class="w"> </span><span class="c1">// 🚩 generic exception</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// In a third:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">UserNotFoundException</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">500</span><span class="p">).</span><span class="na">body</span><span class="p">(</span><span class="s">&#34;Something went wrong&#34;</span><span class="p">);</span><span class="w"> </span><span class="c1">// 🚩 wrong status code</span><span class="w">
</span></span></span></code></pre></div><p><strong>Why it happens:</strong> Exception handling feels like plumbing — unglamorous work that gets deferred and then done inconsistently across a team over months or years.</p>
<p><strong>Why it matters:</strong> Inconsistent error handling makes your API unpredictable for consumers, makes debugging painful, and creates security risk when internal details leak into responses.</p>
<p><strong>The fix:</strong> Define a small hierarchy of domain exceptions (<code>ResourceNotFoundException</code>, <code>ValidationException</code>, <code>ConflictException</code>) and throw those deliberately rather than wrapping everything in <code>RuntimeException</code>. Then handle them all in one place using <code>@RestControllerAdvice</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@RestControllerAdvice</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">GlobalExceptionHandler</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@ExceptionHandler</span><span class="p">(</span><span class="n">ResourceNotFoundException</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">ErrorResponse</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">handleNotFound</span><span class="p">(</span><span class="n">ResourceNotFoundException</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">HttpStatus</span><span class="p">.</span><span class="na">NOT_FOUND</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">body</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">ErrorResponse</span><span class="p">(</span><span class="s">&#34;NOT_FOUND&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">ex</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@ExceptionHandler</span><span class="p">(</span><span class="n">ValidationException</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">ErrorResponse</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">handleValidation</span><span class="p">(</span><span class="n">ValidationException</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">HttpStatus</span><span class="p">.</span><span class="na">BAD_REQUEST</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">body</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">ErrorResponse</span><span class="p">(</span><span class="s">&#34;VALIDATION_ERROR&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">ex</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@ExceptionHandler</span><span class="p">(</span><span class="n">Exception</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">ErrorResponse</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">handleUnexpected</span><span class="p">(</span><span class="n">Exception</span><span class="w"> </span><span class="n">ex</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">log</span><span class="p">.</span><span class="na">error</span><span class="p">(</span><span class="s">&#34;Unexpected error&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">ex</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">status</span><span class="p">(</span><span class="n">HttpStatus</span><span class="p">.</span><span class="na">INTERNAL_SERVER_ERROR</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">.</span><span class="na">body</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">ErrorResponse</span><span class="p">(</span><span class="s">&#34;INTERNAL_ERROR&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;An unexpected error occurred&#34;</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>One place. Consistent responses. No stack traces in production. Done.</p>
<h3 id="mistake-5-treating-applicationproperties-as-a-dumpingground">Mistake 5: Treating <code>application.properties</code> as a dumping ground</h3>
<p>This one starts as a minor annoyance and grows into a genuine operational problem. A file that begins as twelve lines of database config becomes three hundred lines of undocumented magic numbers, environment-specific hacks, feature flags, and credentials that probably shouldn’t be there at all.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># What does this do? Who added it? Is it still needed?</span>
</span></span><span class="line"><span class="cl"><span class="na">some.service.timeout</span><span class="o">=</span><span class="s">5000</span>
</span></span><span class="line"><span class="cl"><span class="na">other.thing.enabled</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl"><span class="na">legacy.mode</span><span class="o">=</span><span class="s">false</span>
</span></span><span class="line"><span class="cl"><span class="na">max.retry.count</span><span class="o">=</span><span class="s">3</span>
</span></span><span class="line"><span class="cl"><span class="na">feature.new-checkout</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl"><span class="na">internal.api.key</span><span class="o">=</span><span class="s">sk_live_abc123...</span>
</span></span></code></pre></div><p><strong>Why it happens:</strong> <code>application.properties</code> is the path of least resistance for any config that doesn&rsquo;t obviously belong somewhere else. Nobody wants to create a new <code>@ConfigurationProperties</code> class for one value.</p>
<p><strong>Why it matters:</strong> At scale, unstructured configuration is an operational risk. Properties get duplicated across environments, credentials end up in version control, values change without anyone knowing what breaks, and onboarding a new developer becomes a treasure hunt.</p>
<p><strong>The fix:</strong> Group related configuration using <code>@ConfigurationProperties</code> with typed, documented classes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@ConfigurationProperties</span><span class="p">(</span><span class="n">prefix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;payment&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nd">@Validated</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kd">record</span> <span class="nc">PaymentProperties</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@NotBlank</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">apiKey</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Min</span><span class="p">(</span><span class="n">1</span><span class="p">)</span><span class="w"> </span><span class="nd">@Max</span><span class="p">(</span><span class="n">30</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">timeoutSeconds</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Min</span><span class="p">(</span><span class="n">0</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">maxRetries</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="p">{}</span><span class="w">
</span></span></span></code></pre></div><p>This gives you type safety, validation on startup, IDE autocomplete, and a single place to document what each value does. Secrets should come from environment variables or a secrets manager — never committed to a repository. And if a property hasn’t been touched in six months, it probably shouldn’t exist.</p>
<h3 id="closing-a-quick-checklist-to-self-audit-yourcodebase">Closing: a quick checklist to self-audit your codebase</h3>
<p>You don’t need to fix everything at once. Run through this in an hour and you’ll know exactly where to focus.</p>
<p><strong>Controllers</strong></p>
<ul>
<li>Do any controllers contain <code>if</code> statements unrelated to HTTP status?</li>
<li>Are there calculations or data transformations happening before a service call?</li>
<li>Is any business rule enforced at the controller level?</li>
</ul>
<p><strong>Transactions</strong></p>
<ul>
<li>Are there <code>@Transactional</code> methods that call external HTTP services?</li>
<li>Are there <code>@Transactional</code> methods calling other <code>@Transactional</code> methods on the same class?</li>
<li>Do you know what state the database is in if any line in a transactional method throws?</li>
</ul>
<p><strong>Dependency injection</strong></p>
<ul>
<li>Are you using field injection (<code>@Autowired</code> on fields)?</li>
<li>Can you instantiate your service classes in a unit test without a Spring context?</li>
<li>Are there any services with more than five or six dependencies?</li>
</ul>
<p><strong>Exception handling</strong></p>
<ul>
<li>Do you have a <code>@RestControllerAdvice</code> that handles all exceptions in one place?</li>
<li>Are you catching <code>Exception</code> in service methods and returning null or swallowing errors?</li>
<li>Are stack traces or internal error messages reaching API responses in production?</li>
</ul>
<p><strong>Configuration</strong></p>
<ul>
<li>Is <code>application.properties</code> longer than 50 lines with no grouping or comments?</li>
<li>Are any secrets or API keys committed to version control?</li>
<li>Are there properties whose purpose isn’t immediately obvious?</li>
</ul>
<p>If you ticked more than three of those boxes, your codebase is normal — this is just what production software looks like after a few years of growth under pressure. The good news is that every one of these issues is fixable incrementally, without a rewrite, one pull request at a time.</p>
]]></content:encoded></item><item><title>SDLC in an Outsourced Company: How the Process Really Works</title><link>https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/</link><pubDate>Sun, 10 May 2026 18:02:25 +0000</pubDate><guid>https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-01.png">&lt;/p>
&lt;p>How distributed teams structure the Software Development Life Cycle — governance models, communication contracts, phase-by-phase responsibilities, and the hidden failure modes that kill outsourced projects before launch.&lt;/p>
&lt;h3 id="what-makes-outsourced-sdlc-different">What makes outsourced SDLC different&lt;/h3>
&lt;p>Every software team runs some version of the SDLC — a structured sequence of phases that takes an idea from concept to running code. When delivery is handled by an external vendor, the same phases exist, but the dynamics between them change completely.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-01.png"></p>
<p>How distributed teams structure the Software Development Life Cycle — governance models, communication contracts, phase-by-phase responsibilities, and the hidden failure modes that kill outsourced projects before launch.</p>
<h3 id="what-makes-outsourced-sdlc-different">What makes outsourced SDLC different</h3>
<p>Every software team runs some version of the SDLC — a structured sequence of phases that takes an idea from concept to running code. When delivery is handled by an external vendor, the same phases exist, but the dynamics between them change completely.</p>
<p>In a co-located team, context flows informally: a developer can tap the product manager on the shoulder, an architect can sketch on a whiteboard in the hallway. In an outsourced engagement, none of that happens. Every shared understanding has to be written down, formally agreed, and deliberately transmitted. What a co-located team resolves in a five-minute hallway conversation requires an async message thread, a clarification document, and a sign-off in outsourcing.</p>
<p>This is not a disadvantage — it is a discipline. Outsourced teams that develop rigorous documentation habits often produce more traceable, better-specified software than co-located teams where “everyone just knows” how things work. The SDLC is the framework that makes this discipline systematic.</p>
<blockquote>
<p>CORE PRINCIPLE</p></blockquote>
<blockquote>
<p>The client owns the <em><strong>what</strong></em> — product vision, business goals, acceptance criteria, and ultimate decision authority. The vendor owns the <em><strong>how</strong></em> — technical approach, tooling choices, implementation detail, and day-to-day engineering execution. The SDLC is the interface between them.</p></blockquote>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-02.png"></p>
<p><em>Figure 1 — The seven SDLC phases in outsourced delivery. Click any phase to explore it. The dashed arc represents the feedback loop from maintenance back into discovery for the next cycle.</em></p>
<h3 id="the-dual-ownership-model">The dual-ownership model</h3>
<p>The most important structural concept in outsourced SDLC is dual ownership: two distinct parties hold responsibility for different layers of the project simultaneously. This is not a hierarchy — the client is not “above” the vendor — it is a division of accountability along a clear interface.</p>
<p>Confusion about ownership is the single most common root cause of outsourced project failure. When both parties believe they are responsible for the same thing (or each believes the other is responsible), gaps appear and deliverables fall through.</p>
<h4 id="client-side">CLIENT SIDE</h4>
<p><strong>Product ownership layer</strong></p>
<ul>
<li>Business requirements and user needs</li>
<li>Acceptance criteria and Definition of Done</li>
<li>Backlog prioritisation and scope decisions</li>
<li>UAT sign-off and production approval</li>
<li>Regulatory and compliance requirements</li>
<li>Budget authority and contract governance</li>
</ul>
<h4 id="vendor-side">VENDOR SIDE</h4>
<p><strong>Engineering execution layer</strong></p>
<ul>
<li>Technical architecture and tooling selection</li>
<li>Sprint planning and capacity management</li>
<li>Code quality, test coverage, and CI/CD</li>
<li>Daily engineering decisions and trade-offs</li>
<li>Infrastructure setup and monitoring</li>
<li>Technical documentation and ADRs</li>
</ul>
<h4 id="shared">SHARED</h4>
<p><strong>Interface &amp; governance</strong></p>
<ul>
<li>Sprint reviews and retrospectives</li>
<li>Risk register and escalation path</li>
<li>Change request evaluation and impact</li>
<li>Definition of Ready for sprint entry</li>
<li>Release decision and go/no-go gates</li>
</ul>
<h4 id="key-roles">KEY ROLES</h4>
<p><strong>Who sits at the interface</strong></p>
<ul>
<li>Client: Product Owner + Technical Lead</li>
<li>Vendor: Project Manager + Tech Lead</li>
<li>These four people own the interface</li>
<li>All escalations route through them first</li>
</ul>
<blockquote>
<p>MOST COMMON FAILURE MODE</p></blockquote>
<blockquote>
<p>Projects fail when the client Product Owner is not genuinely available. If the PO attends only one ceremony per sprint, ambiguities accumulate, the vendor makes assumptions, and by Month 3 the product being built is not the product the client imagined. PO availability is a project risk, not a courtesy.</p></blockquote>
<h3 id="phase-1discovery-planning">Phase 1 — discovery &amp; planning</h3>
<p>Discovery is where the project is defined before a line of code is written. In outsourcing, this phase is often compressed or skipped entirely — and it is the single biggest predictor of whether the engagement will succeed. A well-run discovery produces a shared understanding of what is being built, why it matters, what it does not include, and how success will be measured.</p>
<p>The outputs of discovery are not optional. They form the contractual baseline for the engagement. Any change to scope relative to the discovery outputs is a change request — a formal negotiation, not a verbal agreement.</p>
<h4 id="discovery-outputsthe-mandatory-artifactset">Discovery outputs — the mandatory artifact set</h4>
<ul>
<li>Project Charter — business goals, success metrics, constraints, and named stakeholders with decision authority</li>
<li>High-level roadmap — time-boxed phases with milestone deliverables and payment tranches tied to each</li>
<li>Scope boundary document — an explicit list of what is in scope and what is explicitly out of scope for this engagement</li>
<li>Technical feasibility assessment — vendor’s initial evaluation of risk, unknowns, and proposed technology approach</li>
<li>Communication protocol — meeting cadence, tools, async response SLAs (Service Level Agreement), and escalation path</li>
<li>Definition of Done — the conditions every deliverable must satisfy before the client signs off</li>
</ul>
<blockquote>
<p>TIME INVESTMENT</p></blockquote>
<blockquote>
<p>For a six-month project, expect discovery to take two to four weeks and cost 5–8% of total budget. This is not overhead — it is insurance. Every hour spent in discovery typically saves three to five hours of rework later.</p></blockquote>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-03.png"></p>
<p><em>Figure 2 — Ownership swimlane across SDLC phases. Click any phase row to explore the ownership detail. Every phase ends with a formal handoff artifact — nothing advances without a signed-off output.</em></p>
<h3 id="phase-2requirements">Phase 2 — requirements</h3>
<p>Requirements gathering in outsourcing is a negotiation, not a dictation. The client provides the business intent; the vendor translates it into implementable specifications and raises every ambiguity in writing. The output — typically a Software Requirements Specification or a groomed product backlog with acceptance criteria — is the contract for the build phase.</p>
<p>The two most common failure modes at this phase are: the client providing requirements that are too high-level (“the user should have a good experience”), and the vendor accepting ambiguous requirements without escalating. Both failures are preventable with process discipline.</p>
<blockquote>
<p>REQUIREMENTS QUALITY TEST</p></blockquote>
<blockquote>
<p>Every user story must answer three questions unambiguously: Who is the actor? What do they want to do? Why does it matter to the business? If any of the three is vague, the story is not ready for development. The vendor’s QA on requirements is as important as the client’s initial authorship.</p></blockquote>
<h4 id="the-requirements-clarification-log">The Requirements Clarification Log</h4>
<p>Every ambiguity the vendor identifies must be logged in a shared document — the Requirements Clarification Log — with a status of Open, Answered, or Accepted. This log is reviewed at the start of every sprint planning session. Any story with open clarifications cannot be pulled into the sprint.</p>
<h3 id="phase-3systemdesign">Phase 3 — system design</h3>
<p>System design is where technical architecture is decided and documented before implementation begins. In outsourcing, this phase serves an additional function: it is the moment when the vendor demonstrates technical competence and the client gains confidence (or identifies concerns) in the approach before committing to the build.</p>
<p>The key outputs are <strong>Architecture Decision Records</strong> — short documents that capture each significant technical decision, the alternatives considered, and the rationale for the chosen approach. ADRs are owned by the vendor but approved by the client’s technical lead. They survive team turnover and serve as institutional memory for the lifetime of the codebase.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-04.png"></p>
<p><em>Figure 3 — System design phase: what goes in, what is decided, and what comes out. Every output requires client technical lead approval before development begins.</em></p>
<h3 id="phases-45development-andtesting">Phases 4–5 — development and testing</h3>
<p>Development is typically the longest phase and the one where outsourcing relationships are most stress-tested. The vendor team is executing sprints; the client team is reviewing, testing, and making real-time decisions about scope. The quality of the interface between the two determines whether this phase runs smoothly or deteriorates into confusion and rework.</p>
<p>Testing in an outsourced context has two layers: the vendor is responsible for technical correctness (unit tests, integration tests, regression suites), while the client is responsible for business correctness (User Acceptance Testing). Neither can substitute for the other. A bug-free system that does the wrong thing is a failed deliverable; a system with business-correct behaviour but fragile code is a maintenance liability.</p>
<h4 id="sprint-planning">Sprint planning</h4>
<p><strong>SHARED CEREMONY</strong></p>
<p><em>Stories are pulled from the refined backlog. Vendor estimates and commits. Client PO confirms priorities and clarifies ambiguities before sprint begins — not during it.</em></p>
<h4 id="daily-asyncstandup">Daily async standup</h4>
<p><strong>VENDOR-LED, CLIENT-VISIBLE</strong></p>
<p><em>A structured async update (Slack, Linear, or similar) covering progress, plan, and blockers. Client can read but should not generate work requests through this channel.</em></p>
<h4 id="code-reviewprotocol">Code review protocol</h4>
<p><strong>VENDOR PROCESS</strong></p>
<p><em>Every pull request requires at least one peer review before merge. Client retains repository access and may review code, but day-to-day code review is a vendor responsibility.</em></p>
<h4 id="sprint-review-demo">Sprint review / demo</h4>
<p><strong>SHARED CEREMONY</strong></p>
<p><em>The vendor demos working software against the sprint goal. Client stakeholders provide feedback in the session. Verbal feedback is captured in writing before the session closes.</em></p>
<h4 id="vendor-qa">Vendor QA</h4>
<p><strong>VENDOR-OWNED</strong></p>
<p><em>Unit, integration, and regression tests run in CI on every commit. Test coverage and pass rate are reported to the client at each sprint review as part of the standard dashboard.</em></p>
<h4 id="client-uat">Client UAT</h4>
<p><strong>CLIENT-OWNED</strong></p>
<p><em>Client team tests against acceptance criteria in the staging environment. UAT is time-boxed: a defined window, a defined scope, a written pass/fail result. Unbounded UAT stalls releases.</em></p>
<blockquote>
<p>TESTING ANTI-PATTERN TO AVOID</p></blockquote>
<blockquote>
<p>Never treat UAT as a second round of development. If the client discovers a new requirement during UAT, it is a change request — not a bug. The distinction matters for both schedule and budget. Document the difference in the Definition of Done agreed in Phase 1.</p></blockquote>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-05.png"></p>
<p><em>Figure 4 — The two-week sprint cycle. Ceremonies marked “shared” require both client and vendor attendance. Skipping shared ceremonies is the most reliable way to accumulate hidden misalignment.</em></p>
<h3 id="phases-67deployment-and-maintenance">Phases 6–7 — deployment and maintenance</h3>
<p>Deployment is a production event, and in outsourcing it requires formal client approval before the vendor touches the production environment. This approval should be a written go/no-go decision, not a verbal message. The vendor provides a release runbook — a step-by-step guide to what is being deployed, in what order, with a tested rollback procedure for every step.</p>
<p>Maintenance defines the ongoing relationship after launch. It should be governed by a <strong>Service Level Agreement</strong> that specifies response times for different severity levels, reporting cadence, and the process for prioritising maintenance requests against new feature work. Without a maintenance SLA, the post-launch period becomes a source of friction: the client expects immediate responses; the vendor has moved on to the next engagement.</p>
<blockquote>
<p>MAINTENANCE SLA TIERS — TYPICAL STRUCTURE</p></blockquote>
<blockquote>
<p><strong>P0 — Critical</strong>: system down or data loss risk. Response within 1 hour, resolution target 4 hours. <strong>P1 — High</strong>: major feature broken, no workaround. Response within 4 hours. <strong>P2 — Medium</strong>: feature degraded, workaround exists. Response within 1 business day. <strong>P3 — Low</strong>: cosmetic or minor issue. Response within 1 week.</p></blockquote>
<h4 id="governance-and-sprintcadence">Governance and sprint cadence</h4>
<p>Governance in outsourcing is not bureaucracy — it is the system that keeps the project honest. Without governance, small misalignments compound into large disputes. With governance, problems surface early when they are still cheap to fix.</p>
<p>The minimum viable governance framework for an outsourced engagement has three components: a sprint governance checklist run at every review, a monthly steering committee with executive-level attendance from both parties, and a risk register updated continuously and reviewed formally each month.</p>
<h4 id="sprint-review-governance-checklist">Sprint review governance checklist</h4>
<ul>
<li>All committed stories are either accepted, carried forward with documented reason, or formally descoped — no stories silently dropped</li>
<li>Sprint velocity is reported. If actual velocity is more than 20% below baseline, a root cause is documented and a recovery plan is presented</li>
<li>Test coverage report is shared. No story is accepted without its associated tests in the CI suite</li>
<li>Open bugs are counted and triaged. No P0 or P1 bug is carried forward without a written remediation commitment and target date</li>
<li>Architecture Decision Records are up to date — any design decision made during the sprint is documented before the review closes</li>
<li>Risk register is reviewed. New risks are assigned owners. Mitigations on existing risks are status-checked</li>
<li>Next sprint’s stories are refined, estimated, and in a Definition-of-Ready state before sprint planning begins</li>
<li>Any change requests received during the sprint are logged with estimated impact on scope, schedule, and budget — not silently absorbed</li>
<li>Client retains unrestricted access to the repository, CI/CD pipeline, staging environment, and all project documentation</li>
</ul>
<blockquote>
<p>THE MOST IMPORTANT GOVERNANCE RULE</p></blockquote>
<blockquote>
<p>Every verbal agreement made during a meeting must be captured in writing before the meeting ends — in the meeting notes, in the ticket, or in a follow-up message that is explicitly acknowledged. “We agreed in the call” is not an agreement in outsourcing. Writing is the agreement.</p></blockquote>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/sdlc-in-an-outsourced-company-how-the-process-really-works/image-06.png"></p>
<p><em>Figure 5 — Three-tier governance cadence. Daily standups surface blockers in hours. Fortnightly reviews surface sprint-level misalignment in days. Monthly steering catches strategic drift before it becomes a crisis.</em></p>
<h3 id="closing-what-makes-itwork">Closing: what makes it work</h3>
<p>Outsourced SDLC succeeds when both parties treat the engagement as a collaboration rather than a transaction. The vendor is not a code factory executing a specification; they are a technical partner making hundreds of decisions per week that affect the client’s product. The client is not a passive recipient waiting for deliverables; they are an active participant whose availability, clarity, and decision speed directly determines the project’s velocity.</p>
<p>The SDLC framework described here — with its documentation discipline, formal handoffs, shared ceremonies, and governance cadence — is not bureaucracy for its own sake. It is the minimum structure needed to replace the informal coordination that happens naturally in a co-located team. Every element exists because its absence has been shown, repeatedly, to cause projects to fail.</p>
]]></content:encoded></item><item><title>MVCC - invisible superpower inside every query</title><link>https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/</link><pubDate>Fri, 08 May 2026 19:51:24 +0000</pubDate><guid>https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-01.png">&lt;/p>
&lt;p>Every time two transactions run at the same time in PostgreSQL, something remarkable happens under the hood. Nobody locks the table. Nobody waits. Yet everyone sees a consistent view of the data. This is MVCC — Multi-Version Concurrency Control — and once you understand it, you’ll never look at your database the same way again.&lt;/p>
&lt;h3 id="what-ismvcc">What is MVCC?&lt;/h3>
&lt;p>Imagine a library where every book has a version number. When you check out a book to read it, the librarian doesn’t lock it away from everyone else. Instead, they hand you the version that existed at the moment you arrived. If someone else rewrites the book while you’re reading — you still see the original. You’re reading a snapshot in time.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-01.png"></p>
<p>Every time two transactions run at the same time in PostgreSQL, something remarkable happens under the hood. Nobody locks the table. Nobody waits. Yet everyone sees a consistent view of the data. This is MVCC — Multi-Version Concurrency Control — and once you understand it, you’ll never look at your database the same way again.</p>
<h3 id="what-ismvcc">What is MVCC?</h3>
<p>Imagine a library where every book has a version number. When you check out a book to read it, the librarian doesn’t lock it away from everyone else. Instead, they hand you the version that existed at the moment you arrived. If someone else rewrites the book while you’re reading — you still see the original. You’re reading a snapshot in time.</p>
<p>That’s MVCC. Multi-Version Concurrency Control means PostgreSQL keeps multiple versions of each row simultaneously. Readers see a consistent snapshot of the database at the moment their transaction started. Writers create new versions without touching what readers see. Readers never block writers. Writers never block readers.</p>
<blockquote>
<p><em>In PostgreSQL, every transaction lives in its own bubble of reality — a consistent snapshot of the world at the moment it began.</em></p></blockquote>
<h3 id="the-problem-itsolves">The problem it solves</h3>
<p>The naive approach to concurrent database access is locking — when someone reads a row, lock it so nobody can write it. When someone writes, lock the whole table. This works, but it turns your high-throughput database into a single-file queue. MVCC replaces most of those locks with versioned row history.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-02.png"></p>
<h3 id="the-core-mechanism-transaction-ids">The core mechanism: transaction IDs</h3>
<p>Every transaction in PostgreSQL gets a monotonically increasing transaction ID (called <code>xid</code>). When a row is created or modified, PostgreSQL stamps it with the transaction IDs of who created it and who deleted it. These stamps — stored directly in the row header — are what make MVCC work.</p>
<blockquote>
<p><strong>🔑 THE TWO HIDDEN COLUMNS EVERY ROW CARRIES</strong></p></blockquote>
<blockquote>
<p><code>xmin</code> — the transaction ID that created (inserted) this row version.<br>
<code>xmax</code> — the transaction ID that deleted or superseded this row version. <code>0</code> means the row is still live.</p></blockquote>
<blockquote>
<p>You can actually query these: <code>SELECT xmin, xmax, * FROM your_table;</code></p></blockquote>
<h3 id="two-transactions-one-table-zerodrama">Two transactions, one table, zero drama</h3>
<p>Let’s walk through exactly what happens when two transactions run at the same time. We’ll watch every step — from snapshot creation to commit — with the actual row versions PostgreSQL maintains internally.</p>
<p>Our table: a simple <code>accounts</code> table. Alice has $1000. Transaction A is going to read her balance. Transaction B is going to update it. Watch how they never get in each other&rsquo;s way.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w">      </span><span class="nb">INT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">name</span><span class="w">    </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">balance</span><span class="w"> </span><span class="nb">NUMERIC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Alice&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Inserted by transaction xid=100, so xmin=100, xmax=0
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- What PostgreSQL actually stores internally:
</span></span></span><span class="line"><span class="cl"><span class="c1">-- | xmin | xmax | id | name  | balance |
</span></span></span><span class="line"><span class="cl"><span class="c1">-- |  100 |    0 |  1 | Alice |    1000 |
</span></span></span></code></pre></div><h3 id="the-timelinestep-bystep">The timeline — step by step</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-03.png"></p>
<blockquote>
<p><strong>💡 THE KEY INSIGHT</strong></p></blockquote>
<blockquote>
<p>When Transaction A reads Alice’s balance at T=180ms — <em>after</em> Transaction B has already modified it — it still sees $1000. Because when A started, B’s xid (102) was in the active transaction list. A’s snapshot says: “treat everything from xid=102 as if it doesn’t exist.” This is the snapshot isolation guarantee.</p></blockquote>
<h3 id="how-postgresql-evaluates-row-visibility">How PostgreSQL evaluates row visibility</h3>
<p>For every row a transaction encounters, PostgreSQL runs a visibility check. It’s essentially a set of rules against the row’s <code>xmin</code> and <code>xmax</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">is_visible</span><span class="p">(</span><span class="k">row</span><span class="p">,</span><span class="w"> </span><span class="n">my_snapshot</span><span class="p">):</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">-- Rule 1: creator must have committed before my snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">  </span><span class="k">IF</span><span class="w"> </span><span class="k">row</span><span class="p">.</span><span class="n">xmin</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="n">my_snapshot</span><span class="p">.</span><span class="k">committed</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">RETURN</span><span class="w"> </span><span class="k">false</span><span class="w">   </span><span class="c1">-- creator not committed yet
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">-- Rule 2: creator must not be in my active list
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">  </span><span class="k">IF</span><span class="w"> </span><span class="k">row</span><span class="p">.</span><span class="n">xmin</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="n">my_snapshot</span><span class="p">.</span><span class="n">active</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">RETURN</span><span class="w"> </span><span class="k">false</span><span class="w">   </span><span class="c1">-- creator was in-flight when I started
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">-- Rule 3: if deleted, deleter must NOT have committed before me
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">  </span><span class="k">IF</span><span class="w"> </span><span class="k">row</span><span class="p">.</span><span class="n">xmax</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">IF</span><span class="w"> </span><span class="k">row</span><span class="p">.</span><span class="n">xmax</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="n">my_snapshot</span><span class="p">.</span><span class="k">committed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">AND</span><span class="w"> </span><span class="k">row</span><span class="p">.</span><span class="n">xmax</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="n">my_snapshot</span><span class="p">.</span><span class="n">active</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="k">RETURN</span><span class="w"> </span><span class="k">false</span><span class="w">  </span><span class="c1">-- deleter committed → row is dead to me
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">RETURN</span><span class="w"> </span><span class="k">true</span><span class="w">  </span><span class="c1">-- this row version is visible to me
</span></span></span></code></pre></div><h3 id="row-versions-on-diskwhat-the-heap-actually-lookslike">Row versions on disk — what the heap actually looks like</h3>
<p>PostgreSQL stores rows in heap pages — 8KB chunks on disk. Every row version (called a tuple) has a header containing the MVCC metadata. When you UPDATE a row, the old tuple isn’t erased — it gets a new <code>xmax</code> stamp and a new tuple is inserted right next to it. Both live on the same page (or a new one if it&rsquo;s full).</p>
<blockquote>
<p>📐 TUPLE HEADER FIELDS</p></blockquote>
<blockquote>
<p><code>t_xmin</code> — xid of the inserting transaction | <code>t_xmax</code> — xid of the deleting/updating transaction | <code>t_ctid</code> — physical location (page, offset) of the latest version of this row | <code>t_infomask</code> — bitfield of flags (committed? rolled back? frozen?)</p></blockquote>
<h3 id="watching-the-heap-through-anupdate">Watching the heap through an UPDATE</h3>
<p>Let’s trace the exact state of the heap after each operation on Alice’s row:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-04.png"></p>
<p><em>↑ Transaction A (xid=101) reads Offset 1 only — Offset 2 has xmin=102 which is in A’s active set → invisible</em></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-05.png"></p>
<p><em>↑ Offset 1 is now a dead tuple — wasting space until VACUUM runs. New transactions see only Offset 2.</em></p>
<blockquote>
<p><strong>⚠️ THE UPDATE IS ACTUALLY INSERT + LOGICAL DELETE</strong></p></blockquote>
<blockquote>
<p>PostgreSQL never updates a row in place. Every <code>UPDATE</code> creates a brand new tuple and marks the old one as dead by setting its <code>xmax</code>. This is why long-running UPDATE-heavy workloads cause table bloat — dead tuples accumulate until VACUUM reclaims the space.</p></blockquote>
<h3 id="the-ctidchain">The ctid chain</h3>
<p>Each old tuple’s <code>t_ctid</code> field points to the location of the next version. This creates a version chain that PostgreSQL follows to find the latest valid version of a row. You can inspect this with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- See the hidden MVCC columns on any table
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ctid</span><span class="p">,</span><span class="w">              </span><span class="c1">-- physical location: (page, offset)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">xmin</span><span class="p">,</span><span class="w">              </span><span class="c1">-- creating transaction ID
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">xmax</span><span class="p">,</span><span class="w">              </span><span class="c1">-- deleting transaction ID (0 = live)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">xmin</span><span class="p">::</span><span class="nb">text</span><span class="p">::</span><span class="nb">bigint</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">age</span><span class="p">(</span><span class="n">xmax</span><span class="p">::</span><span class="nb">text</span><span class="p">::</span><span class="n">xid</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">xmax_age</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Result during a concurrent update:
</span></span></span><span class="line"><span class="cl"><span class="c1">-- ctid  | xmin | xmax | id | name  | balance
</span></span></span><span class="line"><span class="cl"><span class="c1">-- (0,1) |  100 |  102 |  1 | Alice |    1000  ← dead, xmax set
</span></span></span><span class="line"><span class="cl"><span class="c1">-- (0,2) |  102 |    0 |  1 | Alice |    1500  ← live, new version
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- See how many dead tuples are in a table
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">n_dead_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">last_autovacuum</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">last_vacuum</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">relname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;accounts&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h3 id="how-updates-persistfrom-buffer-todisk">How updates persist — from buffer to disk</h3>
<p>When Transaction B runs <code>UPDATE accounts SET balance=1500</code>, it doesn&rsquo;t immediately go to the database file on disk. That would be catastrophically slow. Instead, PostgreSQL uses a layered architecture that keeps changes in memory until it&rsquo;s safe and efficient to flush them.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-06.png"></p>
<h3 id="the-walyour-durability-guarantee">The WAL — your durability guarantee</h3>
<p>The Write-Ahead Log is PostgreSQL’s crash safety mechanism. The rule is simple: <em>before any dirty page can be written to the data file, its changes must first be recorded in the WAL and flushed to disk.</em></p>
<p>When you <code>COMMIT</code>, PostgreSQL writes a commit record to WAL and calls <code>fsync()</code>. That&rsquo;s what makes your data durable. If PostgreSQL crashes between commit and checkpoint, recovery replays the WAL to restore the committed state. Your $1500 is safe.</p>
<blockquote>
<p>📌 WHAT HAPPENS DURING THE TRANSACTION (BEFORE COMMIT)</p></blockquote>
<blockquote>
<p>The new tuple (<code>xmin=102, balance=1500</code>) sits in the shared buffer cache with a pending status. The WAL record has been written for crash recovery purposes, but the page isn&rsquo;t flushed to the main data file yet. Other sessions can&rsquo;t see the new version because <code>xmin=102</code> is still in the active transaction list of their snapshots.</p></blockquote>
<h3 id="clogtracking-commitstatus">CLOG — tracking commit status</h3>
<p>How does a transaction know whether <code>xmin=102</code> is committed or rolled back? PostgreSQL maintains the Commit Log (CLOG) — a compact bit array where each transaction ID maps to one of four states:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-07.png"></p>
<p>When a transaction commits, PostgreSQL updates the CLOG bit from <code>IN_PROGRESS</code> to <code>COMMITTED</code>. This is the moment other transactions can start seeing the new tuple. The CLOG update is fast — it&rsquo;s just a bit flip in a shared-memory structure — and PostgreSQL caches the status of recently-seen transactions in tuple header hint bits to avoid CLOG lookups on every read.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Session A: start a transaction and update
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_backend_pid</span><span class="p">(),</span><span class="w"> </span><span class="n">txid_current</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- pid=12345, txid=105
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="o">=</span><span class="mi">1500</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- From Session B (while A is still open):
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">xmin</span><span class="p">,</span><span class="w"> </span><span class="n">xmax</span><span class="p">,</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- xmin=100, xmax=105, balance=1000   ← sees OLD tuple
</span></span></span><span class="line"><span class="cl"><span class="c1">-- The new tuple (xmin=105) exists but is invisible (xid=105 active)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- From Session A itself:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">xmin</span><span class="p">,</span><span class="w"> </span><span class="n">xmax</span><span class="p">,</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- xmin=105, xmax=0, balance=1500  ← sees its OWN pending change
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Now commit in Session A:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- CLOG[105] = COMMITTED
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Session B reads again:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">xmin</span><span class="p">,</span><span class="w"> </span><span class="n">xmax</span><span class="p">,</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- xmin=105, xmax=0, balance=1500  ← now sees the new version
</span></span></span></code></pre></div><h3 id="vacuumthe-janitor-that-nobody-talksabout">VACUUM — the janitor that nobody talks about</h3>
<p>Dead tuples don’t disappear on their own. Every <code>UPDATE</code> and <code>DELETE</code> leaves dead row versions in the heap. If nothing cleaned them up, your tables would grow forever. Enter VACUUM — PostgreSQL&rsquo;s background cleanup process.</p>
<p>VACUUM does several things: it marks dead tuples as free space so new rows can reuse the pages, it updates <code>pg_stat_user_tables</code> with current dead tuple counts, and critically, it advances the transaction ID horizon to prevent XID wraparound — which is the apocalyptic scenario where PostgreSQL has cycled through all 4 billion transaction IDs and can&rsquo;t tell if old data is newer or older than current data.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-08.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Standard VACUUM — marks dead space, doesn&#39;t return to OS
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">VACUUM</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- VACUUM FULL — rewrites table, returns space to OS (locks table!)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">VACUUM</span><span class="w"> </span><span class="k">FULL</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- VACUUM ANALYZE — vacuum + update query planner statistics
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">VACUUM</span><span class="w"> </span><span class="k">ANALYZE</span><span class="w"> </span><span class="n">accounts</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Check autovacuum health — run this regularly in prod
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">schemaname</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">relname</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">table_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">n_dead_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ROUND</span><span class="p">(</span><span class="n">n_dead_tup</span><span class="p">::</span><span class="nb">numeric</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">NULLIF</span><span class="p">(</span><span class="n">n_live_tup</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">dead_pct</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">last_autovacuum</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">autovacuum_count</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- If dead_pct &gt; 20%, autovacuum might not be keeping up
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Tune: autovacuum_vacuum_scale_factor (default 0.2 = 20%)
</span></span></span></code></pre></div><h3 id="the-mentalmodel">The mental model</h3>
<p>MVCC in PostgreSQL is built on four simple ideas that compose into something powerful:</p>
<ol>
<li><strong>Every row version is stamped</strong> with the transaction that created it (<code>xmin</code>) and the one that deleted it (<code>xmax</code>).</li>
<li><strong>Every transaction gets a snapshot</strong> — a list of committed transactions at the moment the snapshot is taken. The snapshot determines what’s visible.</li>
<li><strong>Writers create new versions</strong>, they never overwrite old ones. Readers and writers coexist without locking.</li>
<li><strong>Dead versions accumulate</strong> until VACUUM reclaims them. Monitor your dead tuple ratio in production.</li>
</ol>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/mvcc-invisible-superpower-inside-every-query/image-09.png"></p>
<blockquote>
<p><strong>THE ONE THING TO REMEMBER</strong></p></blockquote>
<blockquote>
<p>“Readers never block writers, writers never block readers.” Everything else in MVCC is the mechanism that makes that true. Understand the snapshot, understand xmin/xmax, and you understand 80% of what PostgreSQL is doing under every query you write.</p></blockquote>
]]></content:encoded></item><item><title>Apache Avro without the Confusion</title><link>https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/</link><pubDate>Fri, 01 May 2026 13:24:41 +0000</pubDate><guid>https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-01.png">&lt;/p>
&lt;p>Binary serialization sounds scary until you realize it’s just a very efficient way of saying “I’ll describe the data once, then send only the bytes I need.” Let’s demystify Avro — from raw bytes to Schema Registry to versioning — with real Java code and zero hand-waving.&lt;/p>
&lt;h3 id="what-is-avro-and-why-should-youcare">What is Avro, and why should you care?&lt;/h3>
&lt;p>Avro is Apache’s answer to “JSON is too fat and XML makes me cry.” It’s a binary serialization formatthat stores data alongside a schema — a JSON document that describes the structure of your data. Think of it as a very strict contract between producer and consumer.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-01.png"></p>
<p>Binary serialization sounds scary until you realize it’s just a very efficient way of saying “I’ll describe the data once, then send only the bytes I need.” Let’s demystify Avro — from raw bytes to Schema Registry to versioning — with real Java code and zero hand-waving.</p>
<h3 id="what-is-avro-and-why-should-youcare">What is Avro, and why should you care?</h3>
<p>Avro is Apache’s answer to “JSON is too fat and XML makes me cry.” It’s a binary serialization formatthat stores data alongside a schema — a JSON document that describes the structure of your data. Think of it as a very strict contract between producer and consumer.</p>
<blockquote>
<p><em>“Avro is what happens when you tell JSON to stop carrying around its key names every single time.”</em></p></blockquote>
<p>Unlike JSON, Avro doesn’t repeat field names with every record. The schema is sent once (or stored in a registry), and the actual data is pure binary — compact, fast, and type-safe. Your Java objects become a stream of bytes that know exactly what they are.</p>
<blockquote>
<p>🎭 THE ELEVATOR PITCH</p></blockquote>
<blockquote>
<p>Imagine sending a spreadsheet. JSON is like printing the column headers on every single row. Avro sends the headers once, then just the values. 10 million rows later, your network team buys you coffee.</p></blockquote>
<h3 id="why-not-just-usejson">Why not just use JSON?</h3>
<p>JSON is great for REST APIs where humans might read the data. But for high-throughput systems — millions of messages per second — JSON is a bandwidth and CPU hog. Avro gives you:</p>
<ul>
<li><strong>~5–10x smaller payload</strong> than equivalent JSON</li>
<li><strong>Schema evolution</strong> — add fields without breaking consumers</li>
<li><strong>Type safety</strong> — no more <code>&quot;count&quot;: &quot;42&quot;</code> sneaking into your integers</li>
<li><strong>Code generation</strong> — compile-time safety with generated Java POJOs</li>
</ul>
<h3 id="where-youll-actually-meet-avro-in-thewild">Where you’ll actually meet Avro in the wild</h3>
<p>You’re not going to randomly pick Avro for your REST API (please don’t). It shines in specific contexts, almost always involving large volumes of structured data moving through a pipeline.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-02.png"></p>
<p>The main places you’ll encounter Avro:</p>
<ul>
<li><strong>Kafka messaging</strong> — the most common use case. Avro is the default serialization format in Confluent’s ecosystem.</li>
<li><strong>Apache Spark</strong> — efficient columnar-style storage and processing of large datasets.</li>
<li><strong>Hadoop / HDFS</strong> — Avro files are a first-class citizen in big data pipelines.</li>
<li><strong>Microservice event buses</strong> — anywhere events need schema contracts enforced at runtime.</li>
<li><strong>Data pipelines (Flink, Debezium)</strong> — CDC and streaming ETL jobs love Avro’s compactness.</li>
</ul>
<h3 id="a-real-avro-schema-fromscratch">A real Avro schema, from scratch</h3>
<p>An Avro schema is just a JSON file that describes your record. Add it to your project under <code>src/main/avro/</code> and the Maven/Gradle plugin generates Java classes for you. Let&rsquo;s model a simple user event.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;record&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;UserEvent&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;namespace&#34;</span><span class="p">:</span> <span class="s2">&#34;com.example.events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;doc&#34;</span><span class="p">:</span> <span class="s2">&#34;Fired every time a user does something interesting&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;fields&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;doc&#34;</span><span class="p">:</span> <span class="s2">&#34;Internal user ID&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;username&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;action&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;enum&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Action&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;symbols&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;LOGIN&#34;</span><span class="p">,</span> <span class="s2">&#34;LOGOUT&#34;</span><span class="p">,</span> <span class="s2">&#34;PURCHASE&#34;</span><span class="p">,</span> <span class="s2">&#34;VIEW&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;timestamp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;logicalType&#34;</span><span class="p">:</span> <span class="s2">&#34;timestamp-millis&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;metadata&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;null&#34;</span><span class="p">,</span> <span class="p">{</span><span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;map&#34;</span><span class="p">,</span> <span class="nt">&#34;values&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span><span class="p">}],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;default&#34;</span><span class="p">:</span> <span class="kc">null</span>  <span class="c1">// nullable field — always put null first in union!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><blockquote>
<p>⚠️ THE NULL UNION TRAP</p></blockquote>
<blockquote>
<p>When you have a nullable field, the union must be <code>[&quot;null&quot;, &quot;YourType&quot;]</code> with <code>null</code> first AND <code>&quot;default&quot;: null</code>. Get this wrong and schema evolution will bite you later. First type in the union = default type.</p></blockquote>
<h3 id="spring-boot-setup-withmaven">Spring Boot setup with Maven</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c">&lt;!-- Avro dependency --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;groupId&gt;</span>org.apache.avro<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;artifactId&gt;</span>avro<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;version&gt;</span>1.11.3<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">&lt;!-- For Kafka: Confluent serializer --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;dependency&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;groupId&gt;</span>io.confluent<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;artifactId&gt;</span>kafka-avro-serializer<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;version&gt;</span>7.5.0<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependency&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">&lt;!-- Code generation plugin --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;plugin&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;groupId&gt;</span>org.apache.avro<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;artifactId&gt;</span>avro-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;version&gt;</span>1.11.3<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;executions&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;execution&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;phase&gt;</span>generate-sources<span class="nt">&lt;/phase&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;goals&gt;&lt;goal&gt;</span>schema<span class="nt">&lt;/goal&gt;&lt;/goals&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;configuration&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;sourceDirectory&gt;</span>src/main/avro<span class="nt">&lt;/sourceDirectory&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;outputDirectory&gt;</span>target/generated-sources<span class="nt">&lt;/outputDirectory&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;stringType&gt;</span>String<span class="nt">&lt;/stringType&gt;</span> <span class="c">&lt;!-- use java.lang.String not CharSequence --&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;/configuration&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/execution&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/executions&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/plugin&gt;</span>
</span></span></code></pre></div><h3 id="using-the-generated-class-in-springboot">Using the generated class in Spring Boot</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kd">@Service</span>
</span></span><span class="line"><span class="cl"><span class="kr">public</span> <span class="kr">class</span> <span class="nx">KafkaProducerService</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">private</span> <span class="nx">final</span> <span class="nx">KafkaTemplate</span><span class="p">&lt;</span><span class="nt">String</span><span class="p">,</span> <span class="na">UserEvent</span><span class="p">&gt;</span> <span class="nx">kafkaTemplate</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">public</span> <span class="k">void</span> <span class="nx">sendUserEvent</span><span class="p">(</span><span class="nx">long</span> <span class="nx">userId</span><span class="p">,</span> <span class="nb">String</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">Action</span> <span class="nx">action</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">UserEvent</span> <span class="nx">event</span> <span class="o">=</span> <span class="nx">UserEvent</span><span class="p">.</span><span class="nx">newBuilder</span><span class="p">()</span>   <span class="c1">// generated Builder pattern
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="p">.</span><span class="nx">setUserId</span><span class="p">(</span><span class="nx">userId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">setUsername</span><span class="p">(</span><span class="nx">username</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">setAction</span><span class="p">(</span><span class="nx">action</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">setTimestamp</span><span class="p">(</span><span class="nx">System</span><span class="p">.</span><span class="nx">currentTimeMillis</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">setMetadata</span><span class="p">(</span><span class="kc">null</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">.</span><span class="nx">build</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nx">kafkaTemplate</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="s2">&#34;user-events&#34;</span><span class="p">,</span> <span class="nb">String</span><span class="p">.</span><span class="nx">valueOf</span><span class="p">(</span><span class="nx">userId</span><span class="p">),</span> <span class="nx">event</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="what-serialized-avro-actually-lookslike">What serialized Avro actually looks like</h3>
<p>Let’s lift the curtain. When Avro serializes a record, it produces a compact binary stream. No field names. No wasted space. Just bytes, in the order the schema defines them.</p>
<h4 id="serializing-manually-no-kafka-no-registry">Serializing manually (no Kafka, no registry)</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.apache.avro.io.*</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.apache.avro.specific.SpecificDatumWriter</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">UserEvent</span><span class="w"> </span><span class="n">event</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">UserEvent</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setUserId</span><span class="p">(</span><span class="n">42L</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setUsername</span><span class="p">(</span><span class="s">&#34;alice&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setAction</span><span class="p">(</span><span class="n">Action</span><span class="p">.</span><span class="na">LOGIN</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setTimestamp</span><span class="p">(</span><span class="n">1700000000000L</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">setMetadata</span><span class="p">(</span><span class="kc">null</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Serialize to bytes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ByteArrayOutputStream</span><span class="w"> </span><span class="n">out</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ByteArrayOutputStream</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">DatumWriter</span><span class="o">&lt;</span><span class="n">UserEvent</span><span class="o">&gt;</span><span class="w"> </span><span class="n">writer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">SpecificDatumWriter</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">UserEvent</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">BinaryEncoder</span><span class="w"> </span><span class="n">encoder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">EncoderFactory</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">binaryEncoder</span><span class="p">(</span><span class="n">out</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">writer</span><span class="p">.</span><span class="na">write</span><span class="p">(</span><span class="n">event</span><span class="p">,</span><span class="w"> </span><span class="n">encoder</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">encoder</span><span class="p">.</span><span class="na">flush</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">out</span><span class="p">.</span><span class="na">toByteArray</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Size: &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">bytes</span><span class="p">.</span><span class="na">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34; bytes&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Hex: &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">HexFormat</span><span class="p">.</span><span class="na">of</span><span class="p">().</span><span class="na">formatHex</span><span class="p">(</span><span class="n">bytes</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Output: 54 0a 616c 6963 65 00 80 b2 ...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// That&#39;s: userId=42, &#34;alice&#34;, action=0(LOGIN), timestamp...</span><span class="w">
</span></span></span></code></pre></div><h4 id="what-those-bytes-actuallymean">What those bytes actually mean</h4>
<p>Let’s decode the binary output of our <code>UserEvent</code> record visually. Avro uses zigzag encoding for integers and length-prefixed strings.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-03.png"></p>
<blockquote>
<p>ZIGZAG ENCODING</p></blockquote>
<blockquote>
<p>Avro encodes integers using zigzag + variable-length encoding. Small numbers take fewer bytes: the number <code>42</code> becomes <code>0x54</code> (one byte!). <code>-1</code> becomes <code>0x01</code>. Large numbers can take up to 10 bytes. There are no wasted sign bits. Clever.</p></blockquote>
<h4 id="with-confluent-schema-registry-magicbyte">With Confluent Schema Registry (magic byte!)</h4>
<p>When using the Confluent serializer with a Schema Registry, 5 extra bytes are prepended:</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-04.png"></p>
<h4 id="deserialization-step-bystep">Deserialization, step by step</h4>
<p>Deserialization is the reverse journey: bytes → Java object. The key insight is that Avro needs <em>two</em> schemas: the writer schema (what was used to write the data) and the reader schema (what your current code expects). When they match, it’s trivial. When they differ, Avro resolves the differences. That’s the magic of schema evolution.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-05.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.apache.avro.io.*</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">org.apache.avro.specific.SpecificDatumReader</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="cm">/* bytes from somewhere */</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">DatumReader</span><span class="o">&lt;</span><span class="n">UserEvent</span><span class="o">&gt;</span><span class="w"> </span><span class="n">reader</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">SpecificDatumReader</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">UserEvent</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">BinaryDecoder</span><span class="w"> </span><span class="n">decoder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DecoderFactory</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">binaryDecoder</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">UserEvent</span><span class="w"> </span><span class="n">event</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">reader</span><span class="p">.</span><span class="na">read</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="n">decoder</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">getUserId</span><span class="p">());</span><span class="w">   </span><span class="c1">// 42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">getUsername</span><span class="p">());</span><span class="w"> </span><span class="c1">// &#34;alice&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="na">getAction</span><span class="p">());</span><span class="w">   </span><span class="c1">// LOGIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// With writer/reader schema resolution (different versions):</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Schema</span><span class="w"> </span><span class="n">writerSchema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="cm">/* schema that was used to write */</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Schema</span><span class="w"> </span><span class="n">readerSchema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">UserEvent</span><span class="p">.</span><span class="na">getClassSchema</span><span class="p">();</span><span class="w"> </span><span class="c1">// your current version</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">DatumReader</span><span class="o">&lt;</span><span class="n">UserEvent</span><span class="o">&gt;</span><span class="w"> </span><span class="n">resolver</span><span class="w"> </span><span class="o">=</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">SpecificDatumReader</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">writerSchema</span><span class="p">,</span><span class="w"> </span><span class="n">readerSchema</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Avro will fill defaults for new fields, skip unknown fields</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// application.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// spring.kafka.consumer.value-deserializer:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">//   io.confluent.kafka.serializers.KafkaAvroDeserializer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// spring.kafka.consumer.properties:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">//   schema.registry.url: http://localhost:8081</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">//   specific.avro.reader: true  ← generate specific class, not GenericRecord</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nd">@KafkaListener</span><span class="p">(</span><span class="n">topics</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;user-events&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">groupId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;my-service&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">handleUserEvent</span><span class="p">(</span><span class="n">UserEvent</span><span class="w"> </span><span class="n">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// Already deserialized! Spring + Confluent did all the heavy lifting.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">log</span><span class="p">.</span><span class="na">info</span><span class="p">(</span><span class="s">&#34;User {} performed {}&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">event</span><span class="p">.</span><span class="na">getUsername</span><span class="p">(),</span><span class="w"> </span><span class="n">event</span><span class="p">.</span><span class="na">getAction</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">userEventProcessor</span><span class="p">.</span><span class="na">process</span><span class="p">(</span><span class="n">event</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="how-to-debug-raw-avroits-not-as-dark-as-itlooks">How to debug raw Avro — it’s not as dark as it looks</h3>
<p>You got a byte array. Your consumer is blowing up. The logs say something unhelpful like <code>EOFException: Unexpected end of stream</code>. Welcome to Avro debugging. Let&rsquo;s fix this.</p>
<blockquote>
<p>🔍 THE DEBUGGING MINDSET</p></blockquote>
<blockquote>
<p>Raw Avro bytes without a schema are like IKEA furniture without instructions. You can tell something should go somewhere, but good luck figuring out which end is up. Always start with the schema.</p></blockquote>
<h4 id="tool-1-avro-tools-official-cli">Tool 1: avro-tools (official CLI)</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Download: https://avro.apache.org/releases.html</span>
</span></span><span class="line"><span class="cl">wget https://downloads.apache.org/avro/avro-1.11.3/java/avro-tools-1.11.3.jar
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect an .avro file (has schema embedded in header)</span>
</span></span><span class="line"><span class="cl">java -jar avro-tools-1.11.3.jar tojson --pretty my-data.avro
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get the schema from an .avro file</span>
</span></span><span class="line"><span class="cl">java -jar avro-tools-1.11.3.jar getschema my-data.avro
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Convert JSON → Avro binary (useful for test data)</span>
</span></span><span class="line"><span class="cl">java -jar avro-tools-1.11.3.jar fromjson <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --schema-file user-event.avsc <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  input.json &gt; output.avro
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check if a file is valid Avro</span>
</span></span><span class="line"><span class="cl">java -jar avro-tools-1.11.3.jar cat my-data.avro <span class="p">|</span> head -5
</span></span></code></pre></div><h4 id="tool-2-decode-in-java-with-genericrecord">Tool 2: Decode in Java with GenericRecord</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// GenericRecord = schema-driven map-like object. No generated class needed.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">suspiciousBytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="cm">/* from Kafka, file, network */</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// If you have the schema:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Schema</span><span class="w"> </span><span class="n">schema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Schema</span><span class="p">.</span><span class="na">Parser</span><span class="p">().</span><span class="na">parse</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">File</span><span class="p">(</span><span class="s">&#34;user-event.avsc&#34;</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">DatumReader</span><span class="o">&lt;</span><span class="n">GenericRecord</span><span class="o">&gt;</span><span class="w"> </span><span class="n">reader</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">GenericDatumReader</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">schema</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">BinaryDecoder</span><span class="w"> </span><span class="n">decoder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">DecoderFactory</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">binaryDecoder</span><span class="p">(</span><span class="n">suspiciousBytes</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">                                                           </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">GenericRecord</span><span class="w"> </span><span class="n">record</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">reader</span><span class="p">.</span><span class="na">read</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="n">decoder</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Walk all fields dynamically</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">schema</span><span class="p">.</span><span class="na">getFields</span><span class="p">().</span><span class="na">forEach</span><span class="p">(</span><span class="n">field</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">printf</span><span class="p">(</span><span class="s">&#34;%-20s = %s%n&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">.</span><span class="na">name</span><span class="p">(),</span><span class="w"> </span><span class="n">record</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">field</span><span class="p">.</span><span class="na">name</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Or convert to JSON for readability</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">String</span><span class="w"> </span><span class="n">json</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">record</span><span class="p">.</span><span class="na">toString</span><span class="p">();</span><span class="w"> </span><span class="c1">// GenericRecord.toString() returns JSON</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">json</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// {&#34;userId&#34;: 42, &#34;username&#34;: &#34;alice&#34;, &#34;action&#34;: &#34;LOGIN&#34;, ...}</span><span class="w">
</span></span></span></code></pre></div><h4 id="tool-3-hex-dump--manualdecode">Tool 3: Hex dump + manual decode</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">hexDump</span><span class="p">(</span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">StringBuilder</span><span class="w"> </span><span class="n">sb</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">StringBuilder</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">bytes</span><span class="p">.</span><span class="na">length</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="n">16</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">0</span><span class="p">)</span><span class="w"> </span><span class="n">sb</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">String</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="s">&#34;%n%04X: &#34;</span><span class="p">,</span><span class="w"> </span><span class="n">i</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">sb</span><span class="p">.</span><span class="na">append</span><span class="p">(</span><span class="n">String</span><span class="p">.</span><span class="na">format</span><span class="p">(</span><span class="s">&#34;%02X &#34;</span><span class="p">,</span><span class="w"> </span><span class="n">bytes</span><span class="o">[</span><span class="n">i</span><span class="o">]</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="n">0xFF</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">sb</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Check for Confluent magic byte</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">bytes</span><span class="o">[</span><span class="n">0</span><span class="o">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">0x00</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">schemaId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ByteBuffer</span><span class="p">.</span><span class="na">wrap</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">4</span><span class="p">).</span><span class="na">getInt</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Confluent format, schema ID: &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">schemaId</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// actual payload starts at byte[5]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">payload</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Arrays</span><span class="p">.</span><span class="na">copyOfRange</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">5</span><span class="p">,</span><span class="w"> </span><span class="n">bytes</span><span class="p">.</span><span class="na">length</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="check-the-schema-type-without-full-deserialization">Check the schema type without full deserialization</h3>
<p>Sometimes you receive Avro bytes on a shared topic and need to decide which deserializer to use — <em>before</em> you know the type. Don’t deserialize everything just to throw it away. Peek at the bytes first.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Confluent wire format: [0x00][schema-id (4 bytes)][avro payload]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="n">Optional</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">peekSchemaName</span><span class="p">(</span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">                                       </span><span class="n">SchemaRegistryClient</span><span class="w"> </span><span class="n">registry</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">bytes</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">bytes</span><span class="p">.</span><span class="na">length</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">5</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">bytes</span><span class="o">[</span><span class="n">0</span><span class="o">]</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="n">0x00</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">Optional</span><span class="p">.</span><span class="na">empty</span><span class="p">();</span><span class="w"> </span><span class="c1">// not Confluent Avro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">schemaId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ByteBuffer</span><span class="p">.</span><span class="na">wrap</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">4</span><span class="p">).</span><span class="na">getInt</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">SchemaMetadata</span><span class="w"> </span><span class="n">meta</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">registry</span><span class="p">.</span><span class="na">getSchemaMetadata</span><span class="p">(</span><span class="n">schemaId</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">Optional</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="n">meta</span><span class="p">.</span><span class="na">getSchemaType</span><span class="p">());</span><span class="w"> </span><span class="c1">// &#34;AVRO&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">Exception</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">Optional</span><span class="p">.</span><span class="na">empty</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Get the full schema without deserializing the payload</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="n">Schema</span><span class="w"> </span><span class="nf">getSchemaFromBytes</span><span class="p">(</span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">SchemaRegistryClient</span><span class="w"> </span><span class="n">registry</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">throws</span><span class="w"> </span><span class="n">Exception</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">schemaId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ByteBuffer</span><span class="p">.</span><span class="na">wrap</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">4</span><span class="p">).</span><span class="na">getInt</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Schema</span><span class="p">.</span><span class="na">Parser</span><span class="p">().</span><span class="na">parse</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">registry</span><span class="p">.</span><span class="na">getById</span><span class="p">(</span><span class="n">schemaId</span><span class="p">).</span><span class="na">toString</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Route messages by schema name - dispatch pattern</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">routeMessage</span><span class="p">(</span><span class="kt">byte</span><span class="o">[]</span><span class="w"> </span><span class="n">bytes</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">schemaId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ByteBuffer</span><span class="p">.</span><span class="na">wrap</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">1</span><span class="p">,</span><span class="w"> </span><span class="n">4</span><span class="p">).</span><span class="na">getInt</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Schema</span><span class="w"> </span><span class="n">schema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">registry</span><span class="p">.</span><span class="na">getById</span><span class="p">(</span><span class="n">schemaId</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="n">schema</span><span class="p">.</span><span class="na">getName</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="s">&#34;UserEvent&#34;</span><span class="w">   </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handleUser</span><span class="p">(</span><span class="n">deserialize</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">UserEvent</span><span class="p">.</span><span class="na">class</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="s">&#34;OrderEvent&#34;</span><span class="w">  </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handleOrder</span><span class="p">(</span><span class="n">deserialize</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">OrderEvent</span><span class="p">.</span><span class="na">class</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="s">&#34;PaymentEvent&#34;</span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handlePayment</span><span class="p">(</span><span class="n">deserialize</span><span class="p">(</span><span class="n">bytes</span><span class="p">,</span><span class="w"> </span><span class="n">PaymentEvent</span><span class="p">.</span><span class="na">class</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">default</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">log</span><span class="p">.</span><span class="na">warn</span><span class="p">(</span><span class="s">&#34;Unknown schema: {}&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">schema</span><span class="p">.</span><span class="na">getName</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="schema-registryand-can-you-live-withoutit">Schema Registry — and can you live without it?</h3>
<p>Schema Registry is a central server that stores and versions your Avro schemas. Instead of embedding the full schema in every message (that would defeat the purpose), you store an integer ID and look up the schema when needed.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-06.png"></p>
<h4 id="can-you-work-without-schema-registry">Can you work without Schema Registry?</h4>
<p>Yes, absolutely. Here are your options:</p>
<h4 id="-option-1-embed-schema-infile">✅ Option 1: Embed schema in file</h4>
<p><em>Avro container files (</em><code>.avro</code><em>) embed the schema in the file header. Works great for batch jobs, HDFS, and Spark. No registry needed.</em></p>
<h4 id="-option-2-schema-in-config--classpath">✅ Option 2: Schema in config / classpath</h4>
<p><em>Ship the</em><code>.avsc</code> <em>file with your app. Every service owns a copy. Simple but schema evolution becomes manual coordination across teams.</em></p>
<h4 id="-option-3-embed-schema-in-everymessage">⚠️ Option 3: Embed schema in every message</h4>
<p><em>Technically works but kills the whole point of Avro. Your 15-byte payload is now 800 bytes. Don’t do this for streaming.</em></p>
<h4 id="-option-4-use-schemaregistry">✅ Option 4: Use Schema Registry</h4>
<p><em>The right choice for Kafka. Confluent’s is free and open-source. Provides compatibility checks, versioning, and central governance. Worth the ops overhead.</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kafka</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">bootstrap-servers</span><span class="p">:</span><span class="w"> </span><span class="l">localhost:9092</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">producer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">value-serializer</span><span class="p">:</span><span class="w"> </span><span class="l">io.confluent.kafka.serializers.KafkaAvroSerializer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">consumer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">value-deserializer</span><span class="p">:</span><span class="w"> </span><span class="l">io.confluent.kafka.serializers.KafkaAvroDeserializer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">schema.registry.url</span><span class="p">:</span><span class="w"> </span><span class="l">http://localhost:8081</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Compatibility mode: BACKWARD, FORWARD, FULL, NONE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">avro.compatibility.level</span><span class="p">:</span><span class="w"> </span><span class="l">BACKWARD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">specific.avro.reader</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># auto.register.schemas: false  ← disable in prod!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Let CI/CD register schemas, not the app at startup</span><span class="w">
</span></span></span></code></pre></div><h3 id="performancegood-and-bad-avro-modelexamples">Performance — good and bad Avro model examples</h3>
<p>Avro is fast <em>by default</em>, but a poorly designed schema can negate all the advantages. The main culprits: excessive unions, deeply nested records, and string fields where you should use enums.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-07.png"></p>
<h4 id="-bad-avroschema">😱 Bad Avro schema</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;status&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span>  <span class="c1">// use enum!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;amount&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span>  <span class="c1">// use double!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;tags&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;array&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;items&#34;</span><span class="p">:</span> <span class="p">{</span>       <span class="c1">// nested unions in
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">[</span>      <span class="c1">// arrays = slow
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="s2">&#34;null&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;string&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;int&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h4 id="-good-avroschema">✅ Good Avro schema</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;status&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;enum&#34;</span><span class="p">,</span>  <span class="c1">// int index = 1 byte
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Status&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;symbols&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;ACTIVE&#34;</span><span class="p">,</span><span class="s2">&#34;INACTIVE&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;amount&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;double&#34;</span>   <span class="c1">// fixed 8 bytes
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;tags&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;array&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;items&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span> <span class="c1">// homogeneous!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;default&#34;</span><span class="p">:</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="performance-rules-ofthumb">Performance rules of thumb</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-08.png"></p>
<blockquote>
<p>CACHE YOUR WRITERS AND READERS</p></blockquote>
<blockquote>
<p><code>DatumWriter</code> and <code>DatumReader</code> are expensive to create. Cache them as singletons or Spring beans. Creating one per message under high load will degrade performance significantly — it involves schema parsing and reflection setup.</p></blockquote>
<h3 id="avro-vs-jsonthe-honest-comparison">Avro vs JSON — the honest comparison</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-09.png"></p>
<blockquote>
<p>🏆 THE VERDICT</p></blockquote>
<blockquote>
<p>Use JSON for REST APIs, config files, and anything humans read. Use Avro for event streaming, big data pipelines, and anywhere you have a schema contract between services producing and consuming millions of records. They’re not competitors — they’re different tools for different jobs.</p></blockquote>
<h3 id="versioninghow-avro-handleschange">Versioning — how Avro handles change</h3>
<p>This is where Avro genuinely shines over JSON. Schema evolution is a first-class feature. The rules are simple: always add fields with defaults, never remove required fields, never change types.</p>
<h4 id="compatibility-modes">Compatibility modes</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-10.png"></p>
<h4 id="safe-schemachanges">Safe schema changes</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="c1">// v1 schema
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span>   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span> <span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;username&#34;</span><span class="p">,</span> <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;action&#34;</span><span class="p">,</span>   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// v2 schema - SAFE: added optional field with default
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span>    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span> <span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;username&#34;</span><span class="p">,</span>  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;action&#34;</span><span class="p">,</span>    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span> <span class="p">}</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;userAgent&#34;</span><span class="p">,</span> <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;null&#34;</span><span class="p">,</span> <span class="s2">&#34;string&#34;</span><span class="p">],</span> <span class="nt">&#34;default&#34;</span><span class="p">:</span> <span class="kc">null</span> <span class="p">}</span><span class="err">,</span> <span class="c1">// ✅
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;version&#34;</span><span class="p">,</span>   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;int&#34;</span><span class="p">,</span> <span class="nt">&#34;default&#34;</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span>  <span class="c1">// ✅
</span></span></span><span class="line"><span class="cl"><span class="c1">// v2 reading v1 data → userAgent=null, version=1 (from defaults)
</span></span></span><span class="line"><span class="cl"><span class="c1">// v1 reading v2 data → userAgent and version ignored
</span></span></span></code></pre></div><h4 id="dangerous-schemachanges">Dangerous schema changes</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="c1">// ❌ BREAKING: renaming a field
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;user_id&#34;</span><span class="p">,</span> <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span> <span class="p">}</span>  <span class="c1">// was &#34;userId&#34; — old data unreadable
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// ❌ BREAKING: changing type
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span> <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span> <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">}</span>  <span class="c1">// was &#34;long&#34; - parse error
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// ❌ BREAKING: removing a required field
</span></span></span><span class="line"><span class="cl"><span class="c1">// (field without default - old data still has it, decoder confused)
</span></span></span><span class="line"><span class="cl"><span class="c1">// ✅ &#34;Renaming&#34; safely via aliases
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;user_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;long&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;aliases&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;userId&#34;</span><span class="p">]</span>   <span class="c1">// reader can find old &#34;userId&#34; field by alias
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ✅ Adding to enum - put new values at the END
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="s2">&#34;symbols&#34;</span><span class="err">:</span> <span class="p">[</span><span class="s2">&#34;LOGIN&#34;</span><span class="p">,</span> <span class="s2">&#34;LOGOUT&#34;</span><span class="p">,</span> <span class="s2">&#34;PURCHASE&#34;</span><span class="p">,</span> <span class="s2">&#34;VIEW&#34;</span><span class="p">,</span> <span class="s2">&#34;SHARE&#34;</span><span class="p">]</span>  <span class="c1">// SHARE is new
</span></span></span><span class="line"><span class="cl"><span class="c1">// Old consumers reading new data get the default enum value
</span></span></span></code></pre></div><h4 id="the-versioning-workflow-with-schemaregistry">The versioning workflow with Schema Registry</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-11.png"></p>
<blockquote>
<p>GOLDEN RULE OF AVRO VERSIONING</p></blockquote>
<blockquote>
<p>Set <code>auto.register.schemas=false</code> in production. Let your CI/CD pipeline register schemas after a compatibility check. If a developer&rsquo;s schema breaks backwards compatibility, you want to catch it in the PR — not at 3am in production.</p></blockquote>
<h4 id="version-badges-what-each-changemeans">Version badges: what each change means</h4>
<h4 id="-non-breaking">✅ Non-breaking</h4>
<p><em>Add optional field with default · Add enum value at end · Add alias · Add doc string</em></p>
<h4 id="-compatible-full">⚠️ Compatible (FULL)</h4>
<p><em>Remove optional field · Widen numeric type (int→long) with resolution</em></p>
<h4 id="-breaking">❌ Breaking</h4>
<p><em>Rename field · Change type · Remove required field · Reorder enum values · Add required field</em></p>
<h3 id="the-cheatsheet">The cheat sheet</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/apache-avro-without-the-confusion/image-12.png"></p>
]]></content:encoded></item><item><title>Java Concurrency without the headache</title><link>https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/</link><pubDate>Sat, 25 Apr 2026 15:45:30 +0000</pubDate><guid>https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-01.png">&lt;/p>
&lt;p>You’ve dealt with threads in production. You’ve seen the race conditions, the deadlocks, the “works on my machine” moments. This is a casual, example-first refresher — not a textbook.&lt;/p>
&lt;h3 id="what-even-is-athread">What even is a Thread?&lt;/h3>
&lt;p>A thread is the smallest unit of execution. Your JVM always has at least one — the main thread. Every thread has its own call stack, but they share heap memory, which is exactly why concurrency is both powerful and terrifying.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-01.png"></p>
<p>You’ve dealt with threads in production. You’ve seen the race conditions, the deadlocks, the “works on my machine” moments. This is a casual, example-first refresher — not a textbook.</p>
<h3 id="what-even-is-athread">What even is a Thread?</h3>
<p>A thread is the smallest unit of execution. Your JVM always has at least one — the main thread. Every thread has its own call stack, but they share heap memory, which is exactly why concurrency is both powerful and terrifying.</p>
<blockquote>
<p><em>“Two threads walk into a bar. The bartender asks who’s first. Neither knows.”</em></p></blockquote>
<p>There are two classic ways to create a thread. Extend <code>Thread</code>, or implement <code>Runnable</code>. In practice you&rsquo;ll almost always use <code>Runnable</code> — or better yet, a lambda.</p>
<h4 id="creating-threads"><strong>Creating threads</strong></h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Option A: extend Thread (old school, limits inheritance)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">class</span> <span class="nc">MyThread</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">Thread</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">run</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;I&#39;m running on: &#34;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="o">+</span><span class="w"> </span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">getName</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Option B: Runnable (preferred - separates task from thread)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Runnable</span><span class="w"> </span><span class="n">task</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Hello from a lambda thread!&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Thread</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(</span><span class="n">task</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;my-worker&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">t</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w">   </span><span class="c1">// spawns a new OS thread - DON&#39;T call run() directly!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">             </span><span class="c1">// calling run() just executes it on the current thread</span><span class="w">
</span></span></span></code></pre></div><h3 id="thread-lifecycle">Thread lifecycle</h3>
<p>A thread goes through these states. It’s not always a straight line — it can bounce between <em>BLOCKED</em>, <em>WAITING</em>, and <em>RUNNABLE</em> while alive.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-02.png"></p>
<h3 id="stopping-a-thread-join">Stopping a thread &amp; join()</h3>
<p>Java deprecated <code>Thread.stop()</code> long ago because it left objects in broken states. The right way is to use an interrupt flag and check it cooperatively inside your loop.</p>
<h4 id="stopping-a-thread-gracefully-join">Stopping a thread gracefully + join()</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Thread</span><span class="w"> </span><span class="n">worker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">isInterrupted</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">doSomeWork</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Thread stopping cleanly&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">worker</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">2000</span><span class="p">);</span><span class="w"> </span><span class="c1">// let it run 2 seconds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">worker</span><span class="p">.</span><span class="na">interrupt</span><span class="p">();</span><span class="w"> </span><span class="c1">// sets the interrupt flag - the thread will see it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">worker</span><span class="p">.</span><span class="na">join</span><span class="p">();</span><span class="w">      </span><span class="c1">// current thread blocks until worker is TERMINATED</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">worker</span><span class="p">.</span><span class="na">join</span><span class="p">(</span><span class="n">5000</span><span class="p">);</span><span class="w"> </span><span class="c1">// or with a 5s timeout - safer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Worker is done, continuing...&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// If your thread sleeps, InterruptedException is thrown when interrupted</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Runnable</span><span class="w"> </span><span class="n">sleeper</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">10_000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">InterruptedException</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">interrupt</span><span class="p">();</span><span class="w"> </span><span class="c1">// re-set the flag!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Woken up early, cleaning up&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">};</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>CLASSIC MISTAKE</strong></em></p></blockquote>
<blockquote>
<p>When catching <code>InterruptedException</code>, always call <code>Thread.currentThread().interrupt()</code>afterward. Catching the exception clears the interrupt flag — if you don&rsquo;t re-set it, the rest of the call stack will never know the thread was interrupted.</p></blockquote>
<h3 id="synchronization-monitors-wait-notify">Synchronization, monitors, wait &amp; notify</h3>
<p>Every Java object has a built-in monitor (also called an intrinsic lock). When you mark a method or block as <code>synchronized</code>, only one thread can hold that lock at a time. Everyone else queues up.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-03.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">BankAccount</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kt">double</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">1000</span><span class="p">.</span><span class="na">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// synchronized method - lock is the BankAccount instance</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">synchronized</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">withdraw</span><span class="p">(</span><span class="kt">double</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">amount</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">balance</span><span class="p">)</span><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">IllegalStateException</span><span class="p">(</span><span class="s">&#34;Insufficient funds&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">balance</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="n">amount</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// synchronized block - more fine-grained, shorter lock duration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">deposit</span><span class="p">(</span><span class="kt">double</span><span class="w"> </span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">doSomeNonCriticalWork</span><span class="p">();</span><span class="w"> </span><span class="c1">// runs without lock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">synchronized</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">balance</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">amount</span><span class="p">;</span><span class="w">  </span><span class="c1">// only this part is protected</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="wait-notify-notifyall">wait(), notify(), notifyAll()</h3>
<p>These methods let threads <em>communicate through a shared object</em>. <code>wait()</code> releases the lock and puts the thread to sleep. <code>notify()</code> wakes one waiting thread. <code>notifyAll()</code> wakes them all. Classic producer-consumer:</p>
<h4 id="producer-consumer-with-waitnotify">Producer-Consumer with wait/notify</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">MessageBus</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">message</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kt">boolean</span><span class="w"> </span><span class="n">hasMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">synchronized</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">produce</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">msg</span><span class="p">)</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">hasMessage</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">wait</span><span class="p">();</span><span class="w"> </span><span class="c1">// buffer full - release lock and sleep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">msg</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">hasMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">notifyAll</span><span class="p">();</span><span class="w"> </span><span class="c1">// wake up consumer(s)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">synchronized</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="nf">consume</span><span class="p">()</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">hasMessage</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">wait</span><span class="p">();</span><span class="w"> </span><span class="c1">// nothing to consume - release lock and sleep</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">hasMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">notifyAll</span><span class="p">();</span><span class="w"> </span><span class="c1">// wake up producer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">message</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Always use while() not if() - spurious wakeups are real!</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>ALWAYS USE WHILE, NOT IF !!!</strong></em></p></blockquote>
<blockquote>
<p>Wrap <code>wait()</code> in a <code>while</code> loop that re-checks the condition. Spurious wakeups (where a thread wakes up for no reason) are a real thing in the JVM spec. A <code>while</code> guards against them; an <code>if</code>doesn&rsquo;t.</p></blockquote>
<h3 id="types-oflocks">Types of Locks</h3>
<p><code>synchronized</code> is simple but limited. <code>java.util.concurrent.locks</code> gives you more power: try to acquire without blocking, set timeouts, choose fairness policy, and separate read from write access.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-04.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">ReentrantLock</span><span class="w"> </span><span class="n">lock</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ReentrantLock</span><span class="p">();</span><span class="w">      </span><span class="c1">// default: non-fair</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ReentrantLock</span><span class="w"> </span><span class="n">fairLock</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ReentrantLock</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w"> </span><span class="c1">// threads served in order</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Always unlock in finally!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">lock</span><span class="p">.</span><span class="na">lock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">doWork</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">lock</span><span class="p">.</span><span class="na">unlock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// tryLock - non-blocking. Perfect for avoiding deadlocks</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">lock</span><span class="p">.</span><span class="na">tryLock</span><span class="p">(</span><span class="n">100</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">MILLISECONDS</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">doWork</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">lock</span><span class="p">.</span><span class="na">unlock</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Couldn&#39;t get the lock - moving on&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// ReadWriteLock - readers don&#39;t block each other</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ReadWriteLock</span><span class="w"> </span><span class="n">rwLock</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ReentrantReadWriteLock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">void</span><span class="w"> </span><span class="nf">read</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">rwLock</span><span class="p">.</span><span class="na">readLock</span><span class="p">().</span><span class="na">lock</span><span class="p">();</span><span class="w">  </span><span class="c1">// many threads can read simultaneously</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="cm">/* read */</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">rwLock</span><span class="p">.</span><span class="na">readLock</span><span class="p">().</span><span class="na">unlock</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">void</span><span class="w"> </span><span class="nf">write</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">val</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">rwLock</span><span class="p">.</span><span class="na">writeLock</span><span class="p">().</span><span class="na">lock</span><span class="p">();</span><span class="w"> </span><span class="c1">// exclusive - blocks all readers too</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="cm">/* write */</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">rwLock</span><span class="p">.</span><span class="na">writeLock</span><span class="p">().</span><span class="na">unlock</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="lock-conditionsupgrade-your-waitnotify">Lock Conditions — upgrade your wait/notify</h3>
<p>Conditions are <code>wait()/notify()</code> but tied to a specific <code>ReentrantLock</code>. The big win: you can have <em>multiple conditions per lock</em> — wake only the producers, or only the consumers, not everyone.</p>
<h4 id="lock-conditionstargetedwake-up">Lock Conditions — targeted wake-up</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">ReentrantLock</span><span class="w"> </span><span class="n">lock</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ReentrantLock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Condition</span><span class="w"> </span><span class="n">notFull</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="n">lock</span><span class="p">.</span><span class="na">newCondition</span><span class="p">();</span><span class="w"> </span><span class="c1">// producers wait here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Condition</span><span class="w"> </span><span class="n">notEmpty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">lock</span><span class="p">.</span><span class="na">newCondition</span><span class="p">();</span><span class="w"> </span><span class="c1">// consumers wait here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Queue</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">buffer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">LinkedList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">int</span><span class="w"> </span><span class="n">capacity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">10</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">void</span><span class="w"> </span><span class="nf">produce</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">item</span><span class="p">)</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">lock</span><span class="p">.</span><span class="na">lock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="na">size</span><span class="p">()</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">capacity</span><span class="p">)</span><span class="w"> </span><span class="n">notFull</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">buffer</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">item</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">notEmpty</span><span class="p">.</span><span class="na">signal</span><span class="p">();</span><span class="w"> </span><span class="c1">// wake ONE consumer, not everyone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">lock</span><span class="p">.</span><span class="na">unlock</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">String</span><span class="w"> </span><span class="nf">consume</span><span class="p">()</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">lock</span><span class="p">.</span><span class="na">lock</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span><span class="w"> </span><span class="n">notEmpty</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">item</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">buffer</span><span class="p">.</span><span class="na">poll</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">notFull</span><span class="p">.</span><span class="na">signal</span><span class="p">();</span><span class="w"> </span><span class="c1">// wake ONE producer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">item</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">lock</span><span class="p">.</span><span class="na">unlock</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="volatilethe-visibility-keyword">Volatile — the visibility keyword</h3>
<p>Without <code>volatile</code>, threads can cache variables in CPU registers and never see updates made by other threads. <code>volatile</code> tells the JVM: <strong>&ldquo;always read this from and write this to main memory.&rdquo;</strong></p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-05.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Worker</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">Runnable</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">volatile</span><span class="w"> </span><span class="kt">boolean</span><span class="w"> </span><span class="n">running</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span><span class="w"> </span><span class="c1">// always read from main memory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">run</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">running</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">doWork</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Stopped!&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">stop</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">running</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span><span class="w"> </span><span class="c1">// visible to all threads immediately</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// volatile also prevents instruction reordering across the write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Useful for the &#34;double-checked locking&#34; singleton pattern:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">class</span> <span class="nc">Singleton</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kd">volatile</span><span class="w"> </span><span class="n">Singleton</span><span class="w"> </span><span class="n">instance</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="n">Singleton</span><span class="w"> </span><span class="nf">getInstance</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">instance</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kd">synchronized</span><span class="w"> </span><span class="p">(</span><span class="n">Singleton</span><span class="p">.</span><span class="na">class</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">instance</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">instance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Singleton</span><span class="p">();</span><span class="w"> </span><span class="c1">// volatile needed here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">instance</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>VOLATILE ≠ ATOMIC</strong></em></p></blockquote>
<blockquote>
<p><code>volatile int count; count++;</code> is still NOT thread-safe. Increment is three operations (read, add, write). Use <code>AtomicInteger</code> instead. <code>volatile</code> only guarantees visibility, not compound atomicity.</p></blockquote>
<h3 id="atomic-variables-cas--cpucache">Atomic variables, CAS &amp; CPU cache</h3>
<p><code>java.util.concurrent.atomic</code> classes use Compare-And-Swap (CAS) — a single CPU instruction. It checks if a value is what you expect; if yes, swaps it atomically. If no (another thread changed it), it retries. No locks. No blocking. Very fast.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-06.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">AtomicInteger</span><span class="w"> </span><span class="n">counter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">AtomicInteger</span><span class="p">(</span><span class="n">0</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">counter</span><span class="p">.</span><span class="na">incrementAndGet</span><span class="p">();</span><span class="w">         </span><span class="c1">// returns new value (++count)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">counter</span><span class="p">.</span><span class="na">getAndIncrement</span><span class="p">();</span><span class="w">         </span><span class="c1">// returns old value (count++)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">counter</span><span class="p">.</span><span class="na">addAndGet</span><span class="p">(</span><span class="n">5</span><span class="p">);</span><span class="w">             </span><span class="c1">// counter += 5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">counter</span><span class="p">.</span><span class="na">compareAndSet</span><span class="p">(</span><span class="n">5</span><span class="p">,</span><span class="w"> </span><span class="n">10</span><span class="p">);</span><span class="w">    </span><span class="c1">// if 5, set to 10 - returns true/false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// AtomicReference for any object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">AtomicReference</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">ref</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">AtomicReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="s">&#34;old&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ref</span><span class="p">.</span><span class="na">compareAndSet</span><span class="p">(</span><span class="s">&#34;old&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;new&#34;</span><span class="p">);</span><span class="w"> </span><span class="c1">// thread-safe swap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// LongAdder - faster than AtomicLong under HIGH contention</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Uses multiple cells internally to reduce CAS collisions</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">LongAdder</span><span class="w"> </span><span class="n">adder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">LongAdder</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">adder</span><span class="p">.</span><span class="na">increment</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">adder</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">42L</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">long</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">adder</span><span class="p">.</span><span class="na">sum</span><span class="p">();</span><span class="w"> </span><span class="c1">// slightly eventual - sums all cells</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// AtomicStampedReference - fixes the ABA problem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// (value changes A→B→A; CAS wrongly thinks nothing changed)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">AtomicStampedReference</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">stamped</span><span class="w"> </span><span class="o">=</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">AtomicStampedReference</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="s">&#34;A&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">0</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">stamped</span><span class="p">.</span><span class="na">compareAndSet</span><span class="p">(</span><span class="s">&#34;A&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;B&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">0</span><span class="p">,</span><span class="w"> </span><span class="n">1</span><span class="p">);</span><span class="w"> </span><span class="c1">// must match stamp too</span><span class="w">
</span></span></span></code></pre></div><h3 id="daemon-threads">Daemon threads</h3>
<p>A daemon thread is a background thread that the JVM will kill as soon as all non-daemon (user) threads finish. It doesn’t prevent the JVM from exiting. Think: garbage collector, log flusher, heartbeat pinger.</p>
<h4 id="user-thread-default">User thread (default)</h4>
<p><em>JVM waits for it to finish. Use for your app’s main work — processing requests, writing to DB, etc.</em></p>
<h4 id="daemon-thread">Daemon thread</h4>
<p><em>JVM kills it abruptly when all user threads exit. Use for background housekeeping that should NOT block shutdown.</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Thread</span><span class="w"> </span><span class="n">heartbeat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="kc">true</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">sendPing</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">5000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">heartbeat</span><span class="p">.</span><span class="na">setDaemon</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w"> </span><span class="c1">// MUST be set before start()!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">heartbeat</span><span class="p">.</span><span class="na">start</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// When main thread ends → JVM exits → heartbeat is killed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// No waiting, no graceful shutdown, just poof</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Check if a thread is daemon:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">boolean</span><span class="w"> </span><span class="n">isDaemon</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">heartbeat</span><span class="p">.</span><span class="na">isDaemon</span><span class="p">();</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>DON’T DO I/O IN DAEMON THREADS</strong></em></p></blockquote>
<blockquote>
<p>Daemon threads get killed mid-operation — no finally blocks, no cleanup. Never do database writes, file flushes, or any state-critical work in a daemon thread. It will be cut off without warning.</p></blockquote>
<h3 id="executorservice-future">ExecutorService &amp; Future</h3>
<p>Creating raw threads is wasteful — each costs ~1MB of stack. <code>ExecutorService</code> manages a pool of reusable threads. You submit tasks; it runs them. Done.</p>
<p><code>Callable</code> is like <code>Runnable</code> but it can return a value and throw checked exceptions. Submitting a <code>Callable</code> gives you a <code>Future</code> — a promise for the result.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">ExecutorService</span><span class="w"> </span><span class="n">pool</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Executors</span><span class="p">.</span><span class="na">newFixedThreadPool</span><span class="p">(</span><span class="n">4</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Submit a Callable - returns a Future</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">future</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pool</span><span class="p">.</span><span class="na">submit</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">1000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="s">&#34;Data from DB&#34;</span><span class="p">;</span><span class="w"> </span><span class="c1">// can return values!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Doing other stuff while DB query runs...&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// get() blocks until done - or throws on error/timeout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">String</span><span class="w"> </span><span class="n">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">future</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">5</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">SECONDS</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">result</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// invokeAll - submit a batch, wait for ALL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Callable</span><span class="o">&lt;</span><span class="n">Integer</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">tasks</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">List</span><span class="p">.</span><span class="na">of</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">computeA</span><span class="p">(),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">computeB</span><span class="p">(),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">computeC</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Integer</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="n">results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pool</span><span class="p">.</span><span class="na">invokeAll</span><span class="p">(</span><span class="n">tasks</span><span class="p">,</span><span class="w"> </span><span class="n">10</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">SECONDS</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// invokeAny - return first result, cancel the rest (hedged requests!)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Integer</span><span class="w"> </span><span class="n">fastest</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pool</span><span class="p">.</span><span class="na">invokeAny</span><span class="p">(</span><span class="n">tasks</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Always shut down! An ExecutorService keeps the JVM alive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">pool</span><span class="p">.</span><span class="na">shutdown</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">pool</span><span class="p">.</span><span class="na">awaitTermination</span><span class="p">(</span><span class="n">30</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">SECONDS</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">pool</span><span class="p">.</span><span class="na">shutdownNow</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="thread-pooltypes">Thread Pool types</h3>
<p>Java gives you factory methods for common pool configurations. In production, you’ll often configure <code>ThreadPoolExecutor</code> directly to know exactly what you&rsquo;re running on.</p>
<h4 id="newfixedthreadpooln">newFixedThreadPool(n)</h4>
<p><em>N threads, unlimited queue. Good for CPU-bound work. Risk: queue grows unbounded under sustained load.</em></p>
<h4 id="newcachedthreadpool">newCachedThreadPool()</h4>
<p><em>Unlimited threads, 60s idle timeout. Good for bursty short tasks. Danger: thread explosion under sustained load.</em></p>
<h4 id="newsinglethreadexecutor">newSingleThreadExecutor()</h4>
<p><em>One thread, sequential. Like a lock-free serial queue. Great for ordered processing.</em></p>
<h4 id="newscheduledthreadpooln">newScheduledThreadPool(n)</h4>
<p><em>Delay and periodic tasks. Replacement for</em> <code>Timer</code><em>— one exception won&rsquo;t kill future runs.</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">ThreadPoolExecutor</span><span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ThreadPoolExecutor</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">4</span><span class="p">,</span><span class="w">                                      </span><span class="c1">// corePoolSize — always-alive threads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">16</span><span class="p">,</span><span class="w">                                     </span><span class="c1">// maxPoolSize — burst capacity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">60L</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">SECONDS</span><span class="p">,</span><span class="w">                  </span><span class="c1">// idle thread keepalive</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">LinkedBlockingQueue</span><span class="o">&lt;&gt;</span><span class="p">(</span><span class="n">200</span><span class="p">),</span><span class="w">        </span><span class="c1">// bounded queue — prevents OOM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">r</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">Thread</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(</span><span class="n">r</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;api-worker-&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">counter</span><span class="p">.</span><span class="na">incrementAndGet</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">t</span><span class="p">.</span><span class="na">setDaemon</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">t</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">ThreadPoolExecutor</span><span class="p">.</span><span class="na">CallerRunsPolicy</span><span class="p">()</span><span class="w"> </span><span class="c1">// backpressure: caller runs it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Rejection policies when queue is full:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// AbortPolicy      → throws RejectedExecutionException (default)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// CallerRunsPolicy → caller thread executes the task (natural throttle)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// DiscardPolicy    → silently drops the task (risky!)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// DiscardOldestPolicy → drops the oldest queued task and retries</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Monitor your pool in production:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">executor</span><span class="p">.</span><span class="na">getActiveCount</span><span class="p">();</span><span class="w">         </span><span class="c1">// threads currently working</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">executor</span><span class="p">.</span><span class="na">getQueue</span><span class="p">().</span><span class="na">size</span><span class="p">();</span><span class="w">        </span><span class="c1">// tasks waiting - this growing = bottleneck!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">executor</span><span class="p">.</span><span class="na">getCompletedTaskCount</span><span class="p">();</span><span class="w">  </span><span class="c1">// total completed tasks</span><span class="w">
</span></span></span></code></pre></div><h3 id="forkjoinpool--recursive-tasks">ForkJoinPool &amp; recursive tasks</h3>
<p>Regular thread pools are bad at recursive divide-and-conquer: a thread splits a task into subtasks and then blocks waiting for them — wasting a thread. <code>ForkJoinPool</code> uses work stealing: idle threads steal subtasks from busy threads&rsquo; queues. Nobody sits idle.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-07.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">java.util.concurrent.*</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">class</span> <span class="nc">SumTask</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">RecursiveTask</span><span class="o">&lt;</span><span class="n">Long</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kt">int</span><span class="o">[]</span><span class="w"> </span><span class="n">data</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">from</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">THRESHOLD</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">1000</span><span class="p">;</span><span class="w"> </span><span class="c1">// split until chunk is small enough</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">SumTask</span><span class="p">(</span><span class="kt">int</span><span class="o">[]</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">from</span><span class="p">,</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">to</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">this</span><span class="p">.</span><span class="na">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">data</span><span class="p">;</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">from</span><span class="p">;</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="na">to</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">to</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">protected</span><span class="w"> </span><span class="n">Long</span><span class="w"> </span><span class="nf">compute</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">to</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">from</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="n">THRESHOLD</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kt">long</span><span class="w"> </span><span class="n">sum</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">from</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">to</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="n">sum</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">data</span><span class="o">[</span><span class="n">i</span><span class="o">]</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">sum</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kt">int</span><span class="w"> </span><span class="n">mid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">from</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">to</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">2</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">SumTask</span><span class="w"> </span><span class="n">left</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">SumTask</span><span class="p">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">from</span><span class="p">,</span><span class="w"> </span><span class="n">mid</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">SumTask</span><span class="w"> </span><span class="n">right</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">SumTask</span><span class="p">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">mid</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">left</span><span class="p">.</span><span class="na">fork</span><span class="p">();</span><span class="w">              </span><span class="c1">// push left to thread pool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kt">long</span><span class="w"> </span><span class="n">rightResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">right</span><span class="p">.</span><span class="na">compute</span><span class="p">();</span><span class="w"> </span><span class="c1">// run right inline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">left</span><span class="p">.</span><span class="na">join</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">rightResult</span><span class="p">;</span><span class="w">   </span><span class="c1">// join left result</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ForkJoinPool</span><span class="w"> </span><span class="n">fjp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ForkJoinPool</span><span class="p">.</span><span class="na">commonPool</span><span class="p">();</span><span class="w"> </span><span class="c1">// shared JVM-wide pool</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">long</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fjp</span><span class="p">.</span><span class="na">invoke</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">SumTask</span><span class="p">(</span><span class="n">bigArray</span><span class="p">,</span><span class="w"> </span><span class="n">0</span><span class="p">,</span><span class="w"> </span><span class="n">bigArray</span><span class="p">.</span><span class="na">length</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// RecursiveAction for void tasks (no return value)</span><span class="w">
</span></span></span></code></pre></div><h3 id="countdownlatch">CountDownLatch</h3>
<p>A <code>CountDownLatch</code> is a one-time gate. You initialize it with a count. Threads call <code>countDown()</code> to decrement it. Anyone waiting on <code>await()</code> is released when it hits zero. Think of it as a race starting pistol — or waiting for all microservices to warm up before accepting traffic.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kt">int</span><span class="w"> </span><span class="n">workerCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">CountDownLatch</span><span class="w"> </span><span class="n">latch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">CountDownLatch</span><span class="p">(</span><span class="n">workerCount</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ExecutorService</span><span class="w"> </span><span class="n">pool</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Executors</span><span class="p">.</span><span class="na">newFixedThreadPool</span><span class="p">(</span><span class="n">workerCount</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">workerCount</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">final</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">i</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">pool</span><span class="p">.</span><span class="na">submit</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">processChunk</span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">latch</span><span class="p">.</span><span class="na">countDown</span><span class="p">();</span><span class="w"> </span><span class="c1">// ALWAYS in finally - even on exception</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">latch</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w">        </span><span class="c1">// main thread blocks here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// latch.await(10, TimeUnit.SECONDS); // or with timeout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;All chunks processed!&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Starting gun pattern - release all threads simultaneously</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">CountDownLatch</span><span class="w"> </span><span class="n">startGun</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">CountDownLatch</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">10</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">startGun</span><span class="p">.</span><span class="na">await</span><span class="p">();</span><span class="w"> </span><span class="c1">// all 10 threads wait here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">runBenchmark</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}).</span><span class="na">start</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">startGun</span><span class="p">.</span><span class="na">countDown</span><span class="p">();</span><span class="w"> </span><span class="c1">// fires - all 10 released at the same instant</span><span class="w">
</span></span></span></code></pre></div><h3 id="semaphore">Semaphore</h3>
<p>A <code>Semaphore</code> controls access to a limited pool of resources. Think: connection pool with N connections, or a rate limiter that caps concurrent requests. It maintains a count of <em>permits</em>. Threads acquire permits; if none are available, they block. When done, they release.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Only 3 DB connections available</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Semaphore</span><span class="w"> </span><span class="n">dbPool</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Semaphore</span><span class="p">(</span><span class="n">3</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">void</span><span class="w"> </span><span class="nf">queryDatabase</span><span class="p">()</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">InterruptedException</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">dbPool</span><span class="p">.</span><span class="na">acquire</span><span class="p">();</span><span class="w">   </span><span class="c1">// blocks if all 3 permits are taken</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">runQuery</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">dbPool</span><span class="p">.</span><span class="na">release</span><span class="p">();</span><span class="w"> </span><span class="c1">// always release in finally!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// tryAcquire - non-blocking version</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">dbPool</span><span class="p">.</span><span class="na">tryAcquire</span><span class="p">(</span><span class="n">500</span><span class="p">,</span><span class="w"> </span><span class="n">TimeUnit</span><span class="p">.</span><span class="na">MILLISECONDS</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">runQuery</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">dbPool</span><span class="p">.</span><span class="na">release</span><span class="p">();</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">returnCachedResult</span><span class="p">();</span><span class="w"> </span><span class="c1">// graceful fallback</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Semaphore(1) = mutex (similar to synchronized, but acquirable across methods)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Semaphore</span><span class="w"> </span><span class="n">mutex</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Semaphore</span><span class="p">(</span><span class="n">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Fair semaphore - threads served in arrival order (no starvation)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Semaphore</span><span class="w"> </span><span class="n">fair</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Semaphore</span><span class="p">(</span><span class="n">5</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h3 id="copyonwrite-collections">CopyOnWrite collections</h3>
<p><code>CopyOnWriteArrayList</code> and <code>CopyOnWriteArraySet</code> trade write performance for read performance. Every write creates a fresh copy of the underlying array. Readers always see a consistent snapshot. Zero locking on reads.</p>
<h4 id="perfect-for">Perfect for…</h4>
<p><em>Event listener lists. Configuration snapshots. Anything read many times, updated rarely (like once per deployment).</em></p>
<h4 id="terrible-for">Terrible for…</h4>
<p><em>Large collections with frequent writes. Every add/remove copies the entire array — O(n) memory allocation per write.</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">CopyOnWriteArrayList</span><span class="o">&lt;</span><span class="n">EventListener</span><span class="o">&gt;</span><span class="w"> </span><span class="n">listeners</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">CopyOnWriteArrayList</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Write - creates a new internal array, old readers unaffected</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">listeners</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="n">myListener</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">listeners</span><span class="p">.</span><span class="na">remove</span><span class="p">(</span><span class="n">oldListener</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Read - snapshot, safe without any locking</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Iterating CopyOnWriteArrayList never throws ConcurrentModificationException</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">EventListener</span><span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">listeners</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">l</span><span class="p">.</span><span class="na">onEvent</span><span class="p">(</span><span class="n">event</span><span class="p">);</span><span class="w"> </span><span class="c1">// concurrent add/remove won&#39;t break this loop</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Regular ArrayList under the same scenario would need:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">EventListener</span><span class="o">&gt;</span><span class="w"> </span><span class="n">synced</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Collections</span><span class="p">.</span><span class="na">synchronizedList</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">ArrayList</span><span class="o">&lt;&gt;</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">synchronized</span><span class="w"> </span><span class="p">(</span><span class="n">synced</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// must manually sync iteration!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">EventListener</span><span class="w"> </span><span class="n">l</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">synced</span><span class="p">)</span><span class="w"> </span><span class="n">l</span><span class="p">.</span><span class="na">onEvent</span><span class="p">(</span><span class="n">event</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h3 id="threadlocal">ThreadLocal</h3>
<p><code>ThreadLocal</code> gives each thread its own isolated copy of a variable. No sharing, no synchronization needed. Classic uses: per-request user context, database connection per thread, or formatters (which aren&rsquo;t thread-safe).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Static ThreadLocal — each thread gets its own value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">static</span><span class="w"> </span><span class="n">ThreadLocal</span><span class="o">&lt;</span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">currentUser</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ThreadLocal</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Set at the start of a request (e.g. in a filter/interceptor)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">currentUser</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">authenticatedUser</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Anywhere deep in the call stack - no need to pass the user around</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">User</span><span class="w"> </span><span class="n">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">currentUser</span><span class="p">.</span><span class="na">get</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// CRITICAL: always clean up! Thread pools reuse threads.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Old value from a previous request will leak to the next!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">currentUser</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="n">user</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">handleRequest</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="k">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">currentUser</span><span class="p">.</span><span class="na">remove</span><span class="p">();</span><span class="w"> </span><span class="c1">// prevents memory leaks in thread pools</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// ThreadLocal with default value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ThreadLocal</span><span class="o">&lt;</span><span class="n">SimpleDateFormat</span><span class="o">&gt;</span><span class="w"> </span><span class="n">formatter</span><span class="w"> </span><span class="o">=</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ThreadLocal</span><span class="p">.</span><span class="na">withInitial</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">SimpleDateFormat</span><span class="p">(</span><span class="s">&#34;yyyy-MM-dd&#34;</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// InheritableThreadLocal - child threads inherit parent&#39;s value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">InheritableThreadLocal</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="n">traceId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">InheritableThreadLocal</span><span class="o">&lt;&gt;</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">traceId</span><span class="p">.</span><span class="na">set</span><span class="p">(</span><span class="s">&#34;req-abc-123&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">new</span><span class="w"> </span><span class="n">Thread</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">traceId</span><span class="p">.</span><span class="na">get</span><span class="p">())).</span><span class="na">start</span><span class="p">();</span><span class="w"> </span><span class="c1">// &#34;req-abc-123&#34;</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>MEMORY LEAK TRAP</strong></em></p></blockquote>
<blockquote>
<p>In a thread pool, threads are reused indefinitely. If you set a <code>ThreadLocal</code> and never call <code>remove()</code>, the value stays attached to that thread forever — causing memory leaks and data leaking between requests. Always clean up in a <code>finally</code> block.</p></blockquote>
<h3 id="virtual-threads-java21">Virtual Threads (Java 21)</h3>
<p>Traditional platform threads map 1:1 to OS threads. 1 MB stack. ~10,000 max before things get painful. Virtual threads are JVM-managed — lightweight, cheap to create by the million. When a virtual thread blocks (I/O, sleep, lock), the JVM <em>unmounts</em> it from the carrier OS thread and parks it. The carrier is free to run another virtual thread. Magic.</p>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-08.png"></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Create a single virtual thread</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Thread</span><span class="w"> </span><span class="n">vt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Thread</span><span class="p">.</span><span class="na">ofVirtual</span><span class="p">().</span><span class="na">name</span><span class="p">(</span><span class="s">&#34;my-vt&#34;</span><span class="p">).</span><span class="na">start</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handleRequest</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Virtual thread per task - executor backed by virtual threads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">try</span><span class="w"> </span><span class="p">(</span><span class="n">ExecutorService</span><span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Executors</span><span class="p">.</span><span class="na">newVirtualThreadPerTaskExecutor</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">Request</span><span class="w"> </span><span class="n">req</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="n">incoming</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">executor</span><span class="p">.</span><span class="na">submit</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handle</span><span class="p">(</span><span class="n">req</span><span class="p">));</span><span class="w"> </span><span class="c1">// each gets a new virtual thread</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w"> </span><span class="c1">// auto-shutdown on close</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Blocking I/O is now FINE - JVM unmounts the virtual thread while waiting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// No need for reactive/async code just to avoid blocking</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Check if a thread is virtual:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Thread</span><span class="p">.</span><span class="na">currentThread</span><span class="p">().</span><span class="na">isVirtual</span><span class="p">();</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// What NOT to do with virtual threads:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// 1. Don&#39;t pin with synchronized + blocking I/O (use ReentrantLock instead)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// 2. Don&#39;t use thread pools of virtual threads (defeats the purpose)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// 3. ThreadLocal still works but be careful with large values at scale</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>WHEN TO USE VIRTUAL THREADS</strong></em></p></blockquote>
<blockquote>
<p>They shine for I/O-bound, high-concurrency work — HTTP servers, database calls, file reads. They don’t help CPU-bound work (you’re still limited by CPU cores). If you’re using Spring Boot 3.2+, you can enable virtual threads with a single property: <code>spring.threads.virtual.enabled=true</code>.</p></blockquote>
<h3 id="scopedvalue-java25">ScopedValue (Java 25)</h3>
<p><code>ScopedValue</code> is the modern answer to <code>ThreadLocal</code>. The problems with <code>ThreadLocal</code>: mutable (anyone can change it), inherited unboundedly (leaks to child threads), and requires manual cleanup. <code>ScopedValue</code> is immutable per scope, automatically scoped to a bounded call tree, and works naturally with virtual threads and structured concurrency.</p>
<h4 id="threadlocal-1">ThreadLocal</h4>
<p><em>Mutable. Lives as long as the thread (or until</em> <code>remove()</code><em>). Can leak. Doesn&rsquo;t compose well with virtual threads at scale.</em></p>
<h4 id="scopedvalue">ScopedValue</h4>
<p><em>Immutable within scope. Auto-cleaned when scope exits. No leaks. Designed for virtual threads and structured concurrency.</em></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Declare a ScopedValue (like a typed key)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">ScopedValue</span><span class="o">&lt;</span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">CURRENT_USER</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ScopedValue</span><span class="p">.</span><span class="na">newInstance</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Bind a value for the duration of a lambda</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ScopedValue</span><span class="p">.</span><span class="na">where</span><span class="p">(</span><span class="n">CURRENT_USER</span><span class="p">,</span><span class="w"> </span><span class="n">authenticatedUser</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">run</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">processRequest</span><span class="p">();</span><span class="w">    </span><span class="c1">// can read CURRENT_USER</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">callServiceLayer</span><span class="p">();</span><span class="w">  </span><span class="c1">// and so can anything it calls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// scope exits → binding is gone. No cleanup needed.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Read it anywhere inside the scope</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kt">void</span><span class="w"> </span><span class="nf">processRequest</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">User</span><span class="w"> </span><span class="n">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">CURRENT_USER</span><span class="p">.</span><span class="na">get</span><span class="p">();</span><span class="w"> </span><span class="c1">// works!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="s">&#34;Processing for: &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">user</span><span class="p">.</span><span class="na">name</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Re-bind for child scope (doesn&#39;t mutate parent)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">ScopedValue</span><span class="p">.</span><span class="na">where</span><span class="p">(</span><span class="n">CURRENT_USER</span><span class="p">,</span><span class="w"> </span><span class="n">adminUser</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">run</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">CURRENT_USER</span><span class="p">.</span><span class="na">get</span><span class="p">();</span><span class="w"> </span><span class="c1">// adminUser here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Back here: CURRENT_USER is still authenticatedUser</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Works great with structured concurrency (Java 21+)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">try</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="n">scope</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">StructuredTaskScope</span><span class="p">.</span><span class="na">ShutdownOnFailure</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">scope</span><span class="p">.</span><span class="na">fork</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">callServiceA</span><span class="p">());</span><span class="w"> </span><span class="c1">// CURRENT_USER is inherited</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">scope</span><span class="p">.</span><span class="na">fork</span><span class="p">(()</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">callServiceB</span><span class="p">());</span><span class="w"> </span><span class="c1">// automatically - no ThreadLocal tricks</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">scope</span><span class="p">.</span><span class="na">join</span><span class="p">().</span><span class="na">throwIfFailed</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><em><strong>STATUS</strong></em></p></blockquote>
<blockquote>
<p>ScopedValue is finalized in Java 25 (JEP 487). It was in preview since Java 21. If you’re on Java 21–24, add <code>--enable-preview</code> to use it. It pairs naturally with <code>StructuredTaskScope</code> for structured concurrency.</p></blockquote>
<h3 id="cheat-sheetpick-yourtool">Cheat sheet — pick your tool</h3>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/java-concurrency-without-the-headache/image-09.png"></p>
]]></content:encoded></item><item><title>When One “Fast” Query Becomes a Production Nightmare: The Dehydration Problem in Hibernate</title><link>https://sukachvitally.github.io/posts/when-one-fast-query-becomes-a-production-nightmare-the-dehydration-problem-in-hibernate/</link><pubDate>Fri, 17 Apr 2026 16:36:30 +0000</pubDate><guid>https://sukachvitally.github.io/posts/when-one-fast-query-becomes-a-production-nightmare-the-dehydration-problem-in-hibernate/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/when-one-fast-query-becomes-a-production-nightmare-the-dehydration-problem-in-hibernate/image-01.png">&lt;/p>
&lt;p>Developers love the idea of a single, elegant SQL query that fetches everything in one go. One round‑trip to the database. One response. One perfect solution.&lt;/p>
&lt;p>Except… in Hibernate, that “perfect” query can quietly turn into a &lt;strong>CPU‑burning, memory‑eating disaster&lt;/strong>.&lt;/p>
&lt;p>This hidden performance trap is known as the &lt;strong>dehydration problem&lt;/strong> — when Hibernate tries to hydrate (map) a multiplied result set into your object graph. The SQL is fast, but the ORM mapping becomes painfully slow.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/when-one-fast-query-becomes-a-production-nightmare-the-dehydration-problem-in-hibernate/image-01.png"></p>
<p>Developers love the idea of a single, elegant SQL query that fetches everything in one go. One round‑trip to the database. One response. One perfect solution.</p>
<p>Except… in Hibernate, that “perfect” query can quietly turn into a <strong>CPU‑burning, memory‑eating disaster</strong>.</p>
<p>This hidden performance trap is known as the <strong>dehydration problem</strong> — when Hibernate tries to hydrate (map) a multiplied result set into your object graph. The SQL is fast, but the ORM mapping becomes painfully slow.</p>
<p>Let’s break down why this happens, how to spot it, and how to avoid it.</p>
<h3 id="the-real-enemy-cartesian-explosion">The Real Enemy: Cartesian Explosion</h3>
<p>The dehydration problem starts with a simple fact:</p>
<blockquote>
<p><em>When you join multiple one‑to‑many or many‑to‑many relations, the database multiplies rows.</em></p></blockquote>
<p>Example domain:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="o">+</span><span class="c1">---------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w">      </span><span class="k">Order</span><span class="w">          </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------|
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w">                  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="nb">date</span><span class="w">                </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">status</span><span class="w">              </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------|
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">items</span><span class="w"> </span><span class="p">(</span><span class="n">IDs</span><span class="w"> </span><span class="k">only</span><span class="p">)</span><span class="w">    </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w">    </span><span class="n">OrderItem</span><span class="w">        </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------|
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w">                  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">orderId</span><span class="w">             </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">productId</span><span class="w">           </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">quantity</span><span class="w">            </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w">      </span><span class="n">Product</span><span class="w">        </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------|
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">id</span><span class="w">                  </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">name</span><span class="w">                </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">price</span><span class="w">               </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------|
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">images</span><span class="w"> </span><span class="p">(</span><span class="n">IDs</span><span class="w"> </span><span class="k">only</span><span class="p">)</span><span class="w">   </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">tags</span><span class="w"> </span><span class="p">(</span><span class="n">IDs</span><span class="w"> </span><span class="k">only</span><span class="p">)</span><span class="w">     </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+
</span></span></span></code></pre></div><p>A typical join:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">order_item</span><span class="w"> </span><span class="n">oi</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">oi</span><span class="p">.</span><span class="n">order_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LEFT</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">product</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">oi</span><span class="p">.</span><span class="n">product_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">:</span><span class="n">id</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>Looks harmless. Runs fast. DB returns rows instantly.</p>
<p>But if:</p>
<ul>
<li>Order has <strong>20 items</strong></li>
<li>Each product has <strong>3 images</strong></li>
<li>Each product has <strong>2 tags</strong></li>
</ul>
<p>Then the join becomes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="mi">20</span><span class="w"> </span><span class="n">items</span><span class="w"> </span><span class="err">×</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="n">images</span><span class="w"> </span><span class="err">×</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="n">tags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">120</span><span class="w"> </span><span class="k">rows</span><span class="w">
</span></span></span></code></pre></div><p>The database doesn’t care — it just streams rows. Hibernate, however, must reconstruct:</p>
<ul>
<li>1 Order</li>
<li>20 OrderItems</li>
<li>20 Products</li>
<li>60 Images</li>
<li>40 Tags</li>
</ul>
<p>…from <strong>120 multiplied rows</strong>.</p>
<p>This is where the trouble begins.</p>
<h3 id="why-hibernate-suffers-hydration-is-expensive">Why Hibernate Suffers: Hydration Is Expensive</h3>
<p>Hibernate must:</p>
<ul>
<li>deduplicate entities</li>
<li>merge rows into collections</li>
<li>maintain identity maps</li>
<li>resolve proxies</li>
<li>apply lazy/eager rules</li>
<li>build the entire object graph</li>
</ul>
<p>This means:</p>
<ul>
<li><strong>High CPU usage</strong></li>
<li><strong>High memory consumption</strong></li>
<li><strong>GC pressure</strong></li>
<li><strong>Slow endpoints</strong></li>
<li><strong>Thread pool starvation</strong></li>
<li>Sometimes even <strong>OutOfMemoryError</strong></li>
</ul>
<p>The SQL query is fast. The <strong>ORM hydration</strong> is slow.</p>
<p>This is the dehydration problem in action.</p>
<h3 id="a-concrete-example-order--orderitems-product">A Concrete Example: Order → OrderItems → Product</h3>
<h3 id="entities-simplified">Entities (simplified)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Order</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@OneToMany</span><span class="p">(</span><span class="n">mappedBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;order&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FetchType</span><span class="p">.</span><span class="na">EAGER</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">List</span><span class="o">&lt;</span><span class="n">OrderItem</span><span class="o">&gt;</span><span class="w"> </span><span class="n">items</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">class</span> <span class="nc">OrderItem</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@ManyToOne</span><span class="p">(</span><span class="n">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FetchType</span><span class="p">.</span><span class="na">EAGER</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Product</span><span class="w"> </span><span class="n">product</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">class</span> <span class="nc">Product</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@OneToMany</span><span class="p">(</span><span class="n">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FetchType</span><span class="p">.</span><span class="na">LAZY</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">List</span><span class="o">&lt;</span><span class="n">ProductImage</span><span class="o">&gt;</span><span class="w"> </span><span class="n">images</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@ManyToMany</span><span class="p">(</span><span class="n">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">FetchType</span><span class="p">.</span><span class="na">LAZY</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">List</span><span class="o">&lt;</span><span class="n">Tag</span><span class="o">&gt;</span><span class="w"> </span><span class="n">tags</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Even if you write a simple query, Hibernate loads the entire graph because of <strong>greedy relations</strong>.</p>
<h3 id="the-result">The result?</h3>
<p>A single “smart” query becomes a <strong>Cartesian explosion</strong> that Hibernate must painfully hydrate.</p>
<h3 id="why-developers-fall-into-thistrap">Why Developers Fall Into This Trap</h3>
<p>Because the SQL looks good.</p>
<ul>
<li>The query is fast.</li>
<li>The DB plan is efficient.</li>
<li>The response time is low.</li>
</ul>
<p>But Hibernate is not a database. It’s an object graph builder.</p>
<p>ORM hydration is the bottleneck — not the SQL.</p>
<h3 id="the-hidden-culprit-greedy-relations">The Hidden Culprit: Greedy Relations</h3>
<p>The dehydration problem almost always appears when entities contain:</p>
<ul>
<li><code>@OneToMany(fetch = EAGER)</code></li>
<li><code>@ManyToOne(fetch = EAGER)</code></li>
<li><code>@ManyToMany(fetch = EAGER)</code></li>
<li>large collections</li>
<li>deep object graphs</li>
<li>bidirectional relations</li>
</ul>
<p>Even if you write a simple query, Hibernate loads everything.</p>
<p>This is why <strong>EAGER fetching is dangerous</strong>.</p>
<h3 id="why-one-query-is-not-alwaysbetter">Why “One Query” Is Not Always Better</h3>
<p>Developers often assume:</p>
<blockquote>
<p><em>“One query is faster than multiple queries.”</em></p></blockquote>
<p>Not in ORM world.</p>
<p>Sometimes <strong>3–5 small queries</strong> are dramatically faster than <strong>one huge join</strong>.</p>
<p>Example:</p>
<ul>
<li>Load Order</li>
<li>Load OrderItems</li>
<li>Load Products</li>
<li>Load Images</li>
<li>Load Tags</li>
</ul>
<p>Each query returns a small, clean dataset. Hibernate hydrates them efficiently.</p>
<p>This is often the correct solution.</p>
<h3 id="how-to-fix-the-dehydration-problem">How to Fix the Dehydration Problem</h3>
<h4 id="1-replace-joins-with-multiple-smallerqueries">1. Replace joins with multiple smaller queries</h4>
<p>Load only what you need, when you need it.</p>
<p>Hibernate handles small result sets extremely well.</p>
<h4 id="2-use-dto-projections-instead-ofentities">2. Use DTO projections instead of entities</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Query</span><span class="p">(</span><span class="s">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">    SELECT new OrderItemDTO(oi.id, p.name, p.price)
</span></span></span><span class="line"><span class="cl"><span class="s">    FROM OrderItem oi
</span></span></span><span class="line"><span class="cl"><span class="s">    JOIN oi.product p
</span></span></span><span class="line"><span class="cl"><span class="s">    WHERE oi.order.id = :orderId
</span></span></span><span class="line"><span class="cl"><span class="s">&#34;&#34;&#34;</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>DTOs avoid hydration entirely.</p>
<h4 id="3-avoid-eagerfetching">3. Avoid EAGER fetching</h4>
<p>Use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@OneToMany</span><span class="p">(</span><span class="n">fetch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">LAZY</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>And load explicitly.</p>
<h4 id="4-apply-ddd-principles">4. Apply DDD principles</h4>
<p>In Domain‑Driven Design:</p>
<ul>
<li>Aggregates should be small</li>
<li>Services should load only the data they own</li>
<li>Bounded contexts should not hydrate entire graphs</li>
</ul>
<p>If your Order aggregate loads:</p>
<ul>
<li>items</li>
<li>products</li>
<li>images</li>
<li>tags</li>
<li>categories</li>
<li>suppliers</li>
</ul>
<p>…it’s too big.</p>
<p>DDD teaches:</p>
<blockquote>
<p><em>Load only what the use case needs.</em></p></blockquote>
<h3 id="how-to-detect-the-problem-in-production">How to Detect the Problem in Production</h3>
<p>Look for:</p>
<ul>
<li>high CPU usage</li>
<li>high memory usage</li>
<li>slow endpoints with “fast” SQL</li>
<li>GC pauses</li>
<li>thread pool exhaustion</li>
<li>huge JSON responses</li>
<li>thread dumps showing <code>org.hibernate.loader.Loader.processResultSet</code></li>
</ul>
<p>This is a clear sign of dehydration.</p>
<h3 id="conclusion">Conclusion</h3>
<p>The dehydration problem is one of the most common — and most misunderstood — performance issues in Hibernate. A single “smart” query can silently trigger a massive Cartesian explosion, causing Hibernate to burn CPU and memory while trying to hydrate an oversized object graph.</p>
<h4 id="key-takeaways">Key takeaways:</h4>
<ul>
<li>SQL can be fast while Hibernate is slow</li>
<li>Cartesian explosions kill performance</li>
<li>EAGER relations are dangerous</li>
<li>Multiple small queries are often faster</li>
<li>DTOs and read models avoid hydration entirely</li>
<li>DDD helps keep aggregates small and focused</li>
</ul>
<p>If you design your domain and queries with these principles in mind, you’ll avoid one of the most painful ORM performance traps — and your production services will thank you.</p>
]]></content:encoded></item><item><title>Business Flow as a Part of Technical Documentation: Why Tech Leads Should Own It</title><link>https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/</link><pubDate>Tue, 14 Apr 2026 17:40:24 +0000</pubDate><guid>https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/image-01.png">&lt;/p>
&lt;p>Every developer has faced the same painful situation: a huge task from a Business Analyst that doesn’t align with the technical architecture, is full of gaps, or ignores corner cases entirely.&lt;/p>
&lt;p>Some developers implement only the “happy path.” Others follow the requirements literally, even when the logic becomes unnecessarily complicated. And after implementation, QA finds a long list of bugs — mostly in those forgotten corner cases.&lt;/p>
&lt;p>Why does this happen? Because the business flow is unclear, incomplete, or not aligned with how the system actually works.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/image-01.png"></p>
<p>Every developer has faced the same painful situation: a huge task from a Business Analyst that doesn’t align with the technical architecture, is full of gaps, or ignores corner cases entirely.</p>
<p>Some developers implement only the “happy path.” Others follow the requirements literally, even when the logic becomes unnecessarily complicated. And after implementation, QA finds a long list of bugs — mostly in those forgotten corner cases.</p>
<p>Why does this happen? Because the business flow is unclear, incomplete, or not aligned with how the system actually works.</p>
<p>So the real question is: <strong>what can we do about it?</strong></p>
<h3 id="the-problem-requirements-without-technical-reality">The Problem: Requirements Without Technical Reality</h3>
<p>When requirements are unclear, developers start asking dozens of questions. This often leads to:</p>
<ul>
<li>endless discussions</li>
<li>huge text descriptions nobody wants to read</li>
<li>BA writing tasks without proper refinement</li>
<li>no long‑term vision of how the feature will evolve</li>
<li>unpredictable complexity during implementation</li>
</ul>
<p>In many cases, BAs don’t have the full requirements in advance. So developers cannot predict how to make the code extensible or in which direction the feature will grow.</p>
<h3 id="a-practical-example-promo-code-validation">A Practical Example: Promo Code Validation</h3>
<p>Imagine a simple feature:</p>
<blockquote>
<p><em>“User enters a promo code during registration.”</em></p></blockquote>
<p>BA writes a task:</p>
<ul>
<li>Validate promo code</li>
<li>Apply bonus</li>
<li>Show error if invalid</li>
</ul>
<p>But they don’t know:</p>
<ul>
<li>validation is done by another service</li>
<li>promo codes can expire</li>
<li>some promo codes require KYC</li>
<li>some promo codes are region‑restricted</li>
<li>some promo codes cannot be used during registration</li>
</ul>
<p>Developers implement the happy path. QA finds 12 bugs. Business says: “We didn’t think about these cases.”</p>
<p>This is exactly where <strong>technical documentation with business flow</strong> saves the project.</p>
<h3 id="step-1-start-with-a-cleanpage">Step 1: Start With a Clean Page</h3>
<p>Create a technical document for the new feature.</p>
<p>Include:</p>
<ul>
<li>general information</li>
<li>goals</li>
<li>links to BA tasks</li>
<li>assumptions</li>
<li>non‑goals</li>
</ul>
<p>This becomes the foundation for the entire team.</p>
<h3 id="step-2-introduce-domainmodels">Step 2: Introduce Domain Models</h3>
<p>Before writing any code, define the domain model.</p>
<h4 id="example-domain-model-plantuml">Example Domain Model (PlantUML)</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/image-02.png"></p>
<p>This already clarifies:</p>
<ul>
<li>what data is needed</li>
<li>what validations exist</li>
<li>what dependencies are involved</li>
</ul>
<h3 id="step-3-use-sequence-diagrams-to-show-the-realflow">Step 3: Use Sequence Diagrams to Show the Real Flow</h3>
<p>This is where PlantUML becomes your best friend.</p>
<h4 id="example-sequencediagram">Example Sequence Diagram</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/business-flow-as-a-part-of-technical-documentation-why-tech-leads-should-own-it/image-03.png"></p>
<p>This diagram shows:</p>
<ul>
<li>dependencies</li>
<li>order of operations</li>
<li>conditional logic</li>
<li>where delays may occur</li>
<li>where errors may appear</li>
</ul>
<p>BAs can read it and finally understand the technical reality. Developers get a clear, visual flow instead of long text. QA gets a map of what to test.</p>
<h3 id="step-4-add-technical-restrictions-and-limitations">Step 4: Add Technical Restrictions and Limitations</h3>
<p>This is the most important part.</p>
<h4 id="example-restrictions">Example Restrictions</h4>
<ul>
<li>Promo validation must be asynchronous.</li>
<li>KYC service has 500ms latency.</li>
<li>Bonus Service rejects expired or region‑restricted promos.</li>
<li>User Service cannot override Bonus Service decisions.</li>
<li>Promo cannot be applied after registration is completed.</li>
</ul>
<p>This prevents unrealistic expectations and protects the team from unnecessary refactoring later.</p>
<h3 id="step-5-review-with-the-bateam">Step 5: Review With the BA Team</h3>
<p>Schedule a short call.</p>
<p>Walk through:</p>
<ul>
<li>the diagrams</li>
<li>the flow</li>
<li>the limitations</li>
<li>the corner cases</li>
</ul>
<p>BAs will usually adjust the business flow after seeing the technical reality. Together, you choose the best solution — not just the fastest or the most “business‑friendly.”</p>
<p>After the review, get approval. Now you have an official document that covers the entire feature.</p>
<h3 id="step-6-use-the-document-as-the-task-description">Step 6: Use the Document as the Task Description</h3>
<p>Instead of writing long Jira descriptions, simply:</p>
<ul>
<li>add a short introduction</li>
<li>attach a link to the documentation</li>
<li>include the relevant documentation block</li>
</ul>
<p>Every developer can open the document and see the full picture.</p>
<p>This also enables TDD or BDD because the flow is fully defined.</p>
<h3 id="step-7-maintain-the-document-afterrelease">Step 7: Maintain the Document After Release</h3>
<p>When the feature goes to production:</p>
<ul>
<li>create a “Bugs” section</li>
<li>add every issue found</li>
<li>note which ones are not bugs</li>
<li>update the flow if needed</li>
</ul>
<p>Support teams can start from this document before asking questions. The documentation evolves naturally and stays alive.</p>
<h3 id="final-benefit">Final Benefit</h3>
<p>BAs become aware of all technical limitations. Next time, there are no surprises like “we need a total refactoring.” And if refactoring is required, it becomes a shared responsibility — not just a development problem.</p>
]]></content:encoded></item><item><title>From Legacy Chaos to Sustainable Architecture: A Step‑by‑Step Refactoring Strategy</title><link>https://sukachvitally.github.io/posts/from-legacy-chaos-to-sustainable-architecture-a-step-by-step-refactoring-strategy/</link><pubDate>Sun, 12 Apr 2026 13:54:17 +0000</pubDate><guid>https://sukachvitally.github.io/posts/from-legacy-chaos-to-sustainable-architecture-a-step-by-step-refactoring-strategy/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/from-legacy-chaos-to-sustainable-architecture-a-step-by-step-refactoring-strategy/image-01.png">&lt;/p>
&lt;p>Legacy systems are rarely “just bad code”. Usually, they’re the result of years of rushed decisions, unclear requirements, and survival‑mode delivery.&lt;/p>
&lt;p>I’ve faced this situation more than once, and developed a practical methodology that lets you &lt;strong>improve the system while still shipping features&lt;/strong> — without a big‑bang rewrite.&lt;/p>
&lt;h3 id="the-three-coreproblems">The three core problems&lt;/h3>
&lt;p>In troubled projects, the same three issues appear again and again:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Business logic is complex and undocumented&lt;/strong> You can’t be sure if a “better” solution is actually correct, because nobody fully understands the current behaviour.&lt;/li>
&lt;li>&lt;strong>The team is under pressure and chooses the easiest path&lt;/strong> Deadlines dominate. Developers implement quick fixes instead of proper solutions, increasing entropy over time.&lt;/li>
&lt;li>&lt;strong>Domain logic is scattered across the codebase&lt;/strong> There is no clear separation of concerns. The same domain is implemented in multiple places, in different ways.&lt;/li>
&lt;/ol>
&lt;p>You can’t fix this with a single refactor. You need a &lt;strong>strategy&lt;/strong>.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/from-legacy-chaos-to-sustainable-architecture-a-step-by-step-refactoring-strategy/image-01.png"></p>
<p>Legacy systems are rarely “just bad code”. Usually, they’re the result of years of rushed decisions, unclear requirements, and survival‑mode delivery.</p>
<p>I’ve faced this situation more than once, and developed a practical methodology that lets you <strong>improve the system while still shipping features</strong> — without a big‑bang rewrite.</p>
<h3 id="the-three-coreproblems">The three core problems</h3>
<p>In troubled projects, the same three issues appear again and again:</p>
<ol>
<li><strong>Business logic is complex and undocumented</strong> You can’t be sure if a “better” solution is actually correct, because nobody fully understands the current behaviour.</li>
<li><strong>The team is under pressure and chooses the easiest path</strong> Deadlines dominate. Developers implement quick fixes instead of proper solutions, increasing entropy over time.</li>
<li><strong>Domain logic is scattered across the codebase</strong> There is no clear separation of concerns. The same domain is implemented in multiple places, in different ways.</li>
</ol>
<p>You can’t fix this with a single refactor. You need a <strong>strategy</strong>.</p>
<h3 id="step-1-enforce-integration-tests-inci">Step 1: Enforce integration tests in CI</h3>
<p>The first move is simple but non‑negotiable:</p>
<ul>
<li><strong>Introduce integration tests</strong> and make them part of the CI pipeline.</li>
<li>The pipeline <strong>must fail</strong> if tests fail.</li>
</ul>
<p>At this stage, don’t aim for perfect coverage. Your goal is to <strong>freeze current behaviour</strong> so you can safely change internals later.</p>
<p>Focus on <strong>functional/integration tests</strong>, not unit tests. Unit tests are too tied to current class structure, which you plan to change.</p>
<h3 id="step-2-require-coverage-for-all-newcode">Step 2: Require coverage for all new code</h3>
<p>Don’t try to cover the entire legacy system at once — that’s a trap.</p>
<p>Instead:</p>
<ul>
<li><strong>Introduce a rule</strong>: all <em>new</em> classes (or significantly new logic) must have at least <strong>80% coverage</strong>.</li>
<li>Keep a file like <code>legacy-classes.txt</code> with all existing uncovered classes.</li>
<li>Configure your coverage tool to <strong>ignore</strong> those legacy classes for now.</li>
</ul>
<p>This way:</p>
<ul>
<li>Legacy code is tolerated but contained.</li>
<li>New code is held to a higher standard.</li>
<li>Over time, the “good” part of the system grows.</li>
</ul>
<h3 id="step-3-touch-legacy-then-testit">Step 3: Touch legacy? Then test it.</h3>
<p>Add one more rule for the team —<em>if you modify legacy code, you must:</em></p>
<ul>
<li>remove the affected classes from <code>legacy-classes.txt</code></li>
<li>add proper tests for them.</li>
</ul>
<p>This gradually converts legacy areas into <strong>tested, safer zones</strong>. You don’t stop feature work — you just <strong>attach improvement to change</strong>.</p>
<p>After a few months, you’ll notice something important: your coverage and confidence have increased <strong>without a big refactor</strong>.</p>
<h3 id="step-4-identify-and-remove-deadcode">Step 4: Identify and remove dead code</h3>
<p>Once you have better test coverage and more confidence, start cleaning up.</p>
<p>A simple approach:</p>
<ol>
<li>Mark suspected legacy code as<code>@Deprecated</code>.</li>
<li>Add an aspect or interceptor that <strong>logs every call</strong> to deprecated methods in production.</li>
<li>Wait a couple of months and analyse the logs.</li>
</ol>
<p>If a deprecated method hasn’t been called in production for a long time, it’s probably dead. Review it carefully — and then <strong>remove it</strong>.</p>
<p>This reduces noise and makes the remaining code easier to reason about.</p>
<h3 id="step-5-group-logic-bydomain">Step 5: Group logic by domain</h3>
<p>Now that the system is safer and cleaner, you can start addressing the real problem: <strong>domain logic scattered everywhere</strong>.</p>
<p>Steps:</p>
<ul>
<li>Identify the <strong>domains</strong> in your system (billing, users, orders, CRM, etc.).</li>
<li>Move domain logic into dedicated modules or packages.</li>
<li>Keep the behaviour the same — your integration tests should still pass.</li>
</ul>
<p>You don’t need heavy refactoring yet. Just <strong>physically group</strong> related logic so each domain has a clear “home”.</p>
<p>Domain‑Driven Design (DDD) concepts help here, but you don’t need to implement full DDD to benefit from <strong>bounded contexts</strong> and <strong>clear boundaries</strong>.</p>
<h3 id="step-6-isolate-data-access-perdomain">Step 6: Isolate data access per domain</h3>
<p>Once domains are clearer, start isolating data access:</p>
<ul>
<li>Create <strong>domain services</strong> that own the logic for a specific domain.</li>
<li>Each service uses its own repositories and exposes <strong>domain models</strong>, not raw entities.</li>
<li>Keep SQL queries inside the domain boundary, not spread across controllers and helpers.</li>
</ul>
<p>At this stage, you may hit performance issues. That’s normal.</p>
<p>Tactics:</p>
<ul>
<li>Keep your database <strong>normalized</strong> as a baseline.</li>
<li>Where performance is critical, introduce <strong>denormalized read models</strong> or <strong>NoSQL stores</strong> for specific use cases.</li>
<li>Ensure denormalized structures are updated within the same transaction or via reliable async mechanisms.</li>
</ul>
<p>The key is to <strong>understand the trade‑offs</strong>, not blindly optimize.</p>
<h3 id="step-7-refine-service-boundaries-andapis">Step 7: Refine service boundaries and APIs</h3>
<p>Now your services are more coherent. Time to make them <strong>consistent</strong>:</p>
<ul>
<li>Each service should own <strong>one domain model</strong> and work with <strong>one type of domain DTO</strong>.</li>
<li>Controllers, listeners, and tasks handle API‑level concerns (validation, mapping, HTTP, messaging).</li>
<li>Services focus on <strong>business rules</strong>, not transport details.</li>
</ul>
<p>Your controllers may look a bit messy for a while. That’s acceptable. The important part is that <strong>business logic is now in one place</strong>.</p>
<p>At this point, you can start documenting the core flows and validating them with product owners. You’re no longer guessing — you have structure.</p>
<h3 id="step-8-evolve-your-publicapi">Step 8: Evolve your public API</h3>
<p>As your internal model stabilizes, you’ll notice:</p>
<ul>
<li>some APIs are redundant,</li>
<li>some can be unified,</li>
<li>some no longer reflect the real domain.</li>
</ul>
<p>Instead of breaking clients, introduce <strong>API versioning</strong>:</p>
<ul>
<li>Design a cleaner <strong>v2 API</strong> aligned with your new domain model.</li>
<li>Keep <strong>v1</strong> for backward compatibility for a while.</li>
<li>Gradually migrate consumers to v2.</li>
</ul>
<p>This lets you modernize without forcing a painful, immediate migration.</p>
<h3 id="step-9-extract-microservices-only-whenready">Step 9: Extract microservices only when ready</h3>
<p>Only now — after:</p>
<ul>
<li>tests are in place,</li>
<li>dead code is removed,</li>
<li>domains are clear,</li>
<li>services are cohesive</li>
</ul>
<p>…does it make sense to talk about <strong>microservices</strong>.</p>
<p>You can:</p>
<ul>
<li>identify services that are truly independent,</li>
<li>give them their own database,</li>
<li>migrate them out carefully.</li>
</ul>
<p>There will still be risks: network latency, distributed transactions, operational overhead. But now you’re moving from a <strong>position of understanding</strong>, not chaos.</p>
<h3 id="final-thought">Final thought</h3>
<p>This methodology is not about hero refactors or perfect architecture. It’s about <strong>creating safety first</strong>, then <strong>improving structure step by step</strong>, while the business keeps moving.</p>
]]></content:encoded></item><item><title>Polymorphism in Database Design: A Simple Key–Value Approach for Evolving Schemas</title><link>https://sukachvitally.github.io/posts/polymorphism-in-database-design-a-simple-key-value-approach-for-evolving-schemas/</link><pubDate>Mon, 06 Apr 2026 10:49:54 +0000</pubDate><guid>https://sukachvitally.github.io/posts/polymorphism-in-database-design-a-simple-key-value-approach-for-evolving-schemas/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/polymorphism-in-database-design-a-simple-key-value-approach-for-evolving-schemas/image-01.png">&lt;/p>
&lt;p>Modern systems evolve fast. New features appear, requirements change, and data models grow in unexpected directions. If you’ve ever worked with a “god table” — a huge table with dozens or hundreds of columns — you know how painful schema evolution can be:&lt;/p>
&lt;ul>
&lt;li>every new feature requires a migration&lt;/li>
&lt;li>many columns stay empty for most rows&lt;/li>
&lt;li>the table becomes harder to maintain&lt;/li>
&lt;li>the schema no longer reflects real business logic&lt;/li>
&lt;/ul>
&lt;p>One way to avoid this is to use &lt;strong>polymorphism in the database&lt;/strong> through a &lt;strong>key–value (EAV) table&lt;/strong>. Instead of adding new columns, you store flexible attributes as rows.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/polymorphism-in-database-design-a-simple-key-value-approach-for-evolving-schemas/image-01.png"></p>
<p>Modern systems evolve fast. New features appear, requirements change, and data models grow in unexpected directions. If you’ve ever worked with a “god table” — a huge table with dozens or hundreds of columns — you know how painful schema evolution can be:</p>
<ul>
<li>every new feature requires a migration</li>
<li>many columns stay empty for most rows</li>
<li>the table becomes harder to maintain</li>
<li>the schema no longer reflects real business logic</li>
</ul>
<p>One way to avoid this is to use <strong>polymorphism in the database</strong> through a <strong>key–value (EAV) table</strong>. Instead of adding new columns, you store flexible attributes as rows.</p>
<p>This article explains how this pattern works, when it’s useful, and where it can go wrong — with a simple example you can study and experiment with.</p>
<h3 id="-what-problem-does-thissolve">🔍 What Problem Does This Solve?</h3>
<p>Imagine you have a table <code>products</code>:</p>
<p>id | name | price | color | size | weight | material | warranty | …</p>
<p>As the business grows, new product types appear:</p>
<ul>
<li>electronics</li>
<li>clothes</li>
<li>furniture</li>
<li>digital goods</li>
</ul>
<p>Each type needs different attributes. You end up adding more and more columns:</p>
<ul>
<li><code>screen_size</code></li>
<li><code>cpu</code></li>
<li><code>ram</code></li>
<li><code>fabric_type</code></li>
<li><code>dimensions</code></li>
<li><code>download_url</code></li>
<li><code>license_key</code></li>
</ul>
<p>Most of these columns are NULL for most rows. Migrations become constant. The schema becomes a mess.</p>
<h3 id="the-polymorphic-keyvalue-solution">The Polymorphic Key–Value Solution</h3>
<p>Instead of adding new columns, we store attributes in a <strong>simple, flexible table</strong>:</p>
<h4 id="product_attributes">product_attributes</h4>
<p><img loading="lazy" src="https://sukachvitally.github.io/posts/polymorphism-in-database-design-a-simple-key-value-approach-for-evolving-schemas/image-02.png"></p>
<h4 id="sql-schema">SQL schema:</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">product_id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">key</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">value</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">type</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w"> </span><span class="c1">-- string, number, boolean, json
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="n">created_at</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>Now you can store <strong>any attribute</strong> without touching the schema.</p>
<h4 id="simple-example-storing-dynamic-productdata">Simple Example: Storing Dynamic Product Data</h4>
<h4 id="insert-attributes-for-atshirt">Insert attributes for a T‑shirt:</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="p">(</span><span class="n">product_id</span><span class="p">,</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;color&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;red&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;size&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;XL&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;material&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;cotton&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h4 id="insert-attributes-for-alaptop">Insert attributes for a laptop:</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="p">(</span><span class="n">product_id</span><span class="p">,</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">11</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;cpu&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;i7&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">11</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;ram&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;16GB&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="mi">11</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;screen_size&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;15.6&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;number&#39;</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h4 id="query-get-all-attributes-for-aproduct">Query: Get all attributes for a product</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">product_attributes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">product_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">11</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><h4 id="query-find-all-products-with-a-specific-attribute">Query: Find all products with a specific attribute</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">product_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">product_attributes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;color&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;red&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>This is the essence of polymorphism: <strong>one table, many types of data, no schema changes.</strong></p>
<h3 id="benefits-of-thisapproach">Benefits of This Approach</h3>
<h4 id="1-no-more-schema-migrations">1. No more schema migrations</h4>
<p>New attributes = new rows, not new columns.</p>
<p>This is extremely useful when:</p>
<ul>
<li>requirements change often</li>
<li>product types vary</li>
<li>you don’t control the schema (SaaS, plugins, integrations)</li>
</ul>
<h4 id="2-flexible-and-futureproof">2. Flexible and future‑proof</h4>
<p>You can add attributes dynamically:</p>
<ul>
<li>from admin panels</li>
<li>from configuration</li>
<li>from user input</li>
<li>from external systems</li>
</ul>
<p>No DBA involvement needed.</p>
<h4 id="3-works-well-for-sparsedata">3. Works well for sparse data</h4>
<p>If only 5% of products have a <code>screen_size</code>, why create a column for everyone?</p>
<h4 id="4-great-for-prototyping">4. Great for prototyping</h4>
<p>You can start fast without designing a perfect schema.</p>
<h4 id="5-easy-to-extend-withjson">5. Easy to extend with JSON</h4>
<p>You can store complex values:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="p">(</span><span class="n">product_id</span><span class="p">,</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">12</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;dimensions&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;{&#34;w&#34;: 100, &#34;h&#34;: 50}&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;json&#39;</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h3 id="pitfalls-and-how-to-avoidthem">Pitfalls and How to Avoid Them</h3>
<p>This pattern is powerful — but dangerous if misused.</p>
<h4 id="1-harder-toquery">1. Harder to query</h4>
<p>Filtering becomes more complex:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">products</span><span class="w"> </span><span class="n">p</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="n">a1</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">a1</span><span class="p">.</span><span class="n">product_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">a1</span><span class="p">.</span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;color&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">a1</span><span class="p">.</span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;red&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">product_attributes</span><span class="w"> </span><span class="n">a2</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">a2</span><span class="p">.</span><span class="n">product_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">a2</span><span class="p">.</span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;size&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">a2</span><span class="p">.</span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;XL&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p><strong>Solution:</strong> Use materialized views or caching for common queries.</p>
<h4 id="2-no-strongtyping">2. No strong typing</h4>
<p>Everything is stored as text unless you enforce types manually.</p>
<p><strong>Solution:</strong> Use a <code>type</code> column and validate in the application layer.</p>
<h4 id="3-harder-to-enforce-constraints">3. Harder to enforce constraints</h4>
<p>You can’t easily enforce:</p>
<ul>
<li>required attributes</li>
<li>allowed values</li>
<li>foreign keys</li>
</ul>
<p><strong>Solution:</strong> Use application‑level validation or attribute metadata tables.</p>
<h4 id="4-performance-issues-atscale">4. Performance issues at scale</h4>
<p>A product with 50 attributes = 50 rows. A million products = 50 million rows.</p>
<p><strong>Solution:</strong></p>
<ul>
<li>add indexes on <code>(product_id)</code> and <code>(key, value)</code></li>
<li>shard by product type</li>
<li>use caching (Redis)</li>
</ul>
<h4 id="5-overuse-leads-tochaos">5. Overuse leads to chaos</h4>
<p>If <em>everything</em> becomes key–value, you lose structure.</p>
<p><strong>Solution:</strong> Use this pattern only for <strong>dynamic</strong> or <strong>optional</strong> attributes. Keep core fields in the main table.</p>
<h3 id="when-should-you-use-thispattern">When Should You Use This Pattern?</h3>
<h4 id="good-usecases">Good use cases:</h4>
<ul>
<li>product catalogs</li>
<li>CMS metadata</li>
<li>user preferences</li>
<li>feature flags</li>
<li>IoT sensor data</li>
<li>audit logs</li>
<li>dynamic forms</li>
</ul>
<h4 id="bad-usecases">Bad use cases:</h4>
<ul>
<li>financial transactions</li>
<li>core business entities</li>
<li>data requiring strict validation</li>
<li>heavy analytical workloads</li>
</ul>
<h3 id="final-thoughts">Final Thoughts</h3>
<p>Polymorphism in database design — especially through a key–value table — is a powerful tool for building flexible, evolving systems. It helps avoid schema bloat, reduces migration overhead, and supports dynamic data models.</p>
<p>But like any abstraction, it must be used intentionally.</p>
<p><strong>Use it for dynamic, optional, or unpredictable attributes.</strong> <strong>Avoid it for core, validated, business‑critical data.</strong></p>
<p>If you balance flexibility with structure, this pattern becomes a reliable foundation for scalable, maintainable systems — especially in fast‑moving domains like e‑commerce, gaming, SaaS, and content platforms.</p>
]]></content:encoded></item><item><title>SPA SEO Routing with Kubernetes Ingress Controller</title><link>https://sukachvitally.github.io/posts/spa-seo-routing-with-kubernetes-ingress-controller/</link><pubDate>Fri, 02 Feb 2024 12:50:25 +0000</pubDate><guid>https://sukachvitally.github.io/posts/spa-seo-routing-with-kubernetes-ingress-controller/</guid><description>&lt;p>&lt;img loading="lazy" src="https://sukachvitally.github.io/posts/spa-seo-routing-with-kubernetes-ingress-controller/image-01.png">&lt;/p>
&lt;h3 id="problem">Problem:&lt;/h3>
&lt;p>SPA applications face challenges with SEO as they typically return &lt;code>index.html&lt;/code> for all requests. While one solution involves proxying all requests through a backend application to render SPA layout or traditional HTML for robots, this introduces complexity and disadvantages.&lt;/p>
&lt;h3 id="kubernetes-ingress">Kubernetes ingress&lt;/h3>
&lt;p>Fortunately, Kubernetes’ Ingress controller provides logic to address this issue without additional complexity. Annotations can be used to configure routing for the Ingress.&lt;/p>
&lt;p>As an example, let’s redirect all robot requests to our &lt;code>seo-service&lt;/code>.&lt;/p></description><content:encoded><![CDATA[<p><img loading="lazy" src="https://sukachvitally.github.io/posts/spa-seo-routing-with-kubernetes-ingress-controller/image-01.png"></p>
<h3 id="problem">Problem:</h3>
<p>SPA applications face challenges with SEO as they typically return <code>index.html</code> for all requests. While one solution involves proxying all requests through a backend application to render SPA layout or traditional HTML for robots, this introduces complexity and disadvantages.</p>
<h3 id="kubernetes-ingress">Kubernetes ingress</h3>
<p>Fortunately, Kubernetes’ Ingress controller provides logic to address this issue without additional complexity. Annotations can be used to configure routing for the Ingress.</p>
<p>As an example, let’s redirect all robot requests to our <code>seo-service</code>.</p>
<h3 id="ingress-controller-example">Ingress Controller Example:</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">main-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/rewrite-target</span><span class="p">:</span><span class="w"> </span><span class="l">$uri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/server-snippet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      set $prerender 0;
</span></span></span><span class="line"><span class="cl"><span class="sd">      if ($http_user_agent ~* &#34;googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator&#34;) {
</span></span></span><span class="line"><span class="cl"><span class="sd">          set $prerender 1;
</span></span></span><span class="line"><span class="cl"><span class="sd">      }
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      if ($uri ~* &#34;\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)&#34;) {
</span></span></span><span class="line"><span class="cl"><span class="sd">        set $prerender 0;
</span></span></span><span class="line"><span class="cl"><span class="sd">      }
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      if ($prerender = 1) {            
</span></span></span><span class="line"><span class="cl"><span class="sd">        rewrite ^(.*)$ /seo$1 break;      
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">yoursite.org</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/api</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">backend-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">30200</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/seo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">seo-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">30201</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">frontend-service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">30202</span><span class="w">
</span></span></span></code></pre></div><h3 id="explanation">Explanation:</h3>
<p><strong>Services:</strong></p>
<ul>
<li><code>backend-service</code>: Backend application providing API for SPA.</li>
<li><code>seo-service</code>: Backend application rendering traditional HTML for robots.</li>
<li><code>frontend-service</code>: Static storage serving SPA files (images, CSS, JS, etc.).</li>
</ul>
<p><strong>Rules Block:</strong></p>
<ul>
<li>Responsible for routing requests based on their paths.</li>
</ul>
<p><strong>Annotation Block:</strong></p>
<ul>
<li>Annotations are handled before routing, allowing us to modify routing.</li>
<li><code>nginx.ingress.kubernetes.io/rewrite-target: $uri</code>: Adds the request path to each routed request.</li>
<li><code>nginx.ingress.kubernetes.io/server-snippet</code>: Contains logic to route robot requests to the <code>seo-service</code> by adding the <code>/seo</code> prefix.</li>
</ul>
<h3 id="conclusion">Conclusion:</h3>
<p>In conclusion, Kubernetes Ingress controllers offer an efficient way to make SPAs more SEO-friendly with minimal effort.</p>
]]></content:encoded></item><item><title>About me</title><link>https://sukachvitally.github.io/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://sukachvitally.github.io/about/</guid><description>&lt;p>Hi, I&amp;rsquo;m &lt;strong>Vitalii&lt;/strong> 👋&lt;/p>
&lt;p>I&amp;rsquo;m a Java developer, and I&amp;rsquo;ve been writing backend code for more than 12 years: mostly Spring Boot, microservices, PostgreSQL, Kafka and everything that keeps them running in the cloud.&lt;/p>
&lt;p>My favourite hobby is keeping up with what&amp;rsquo;s new: new Java releases, new tools, new trends. Lately that&amp;rsquo;s been a lot of AI tooling like Claude Code, MCP and RAG. Whenever I learn something, I try it out for real, and then write about it here. If something took me a weekend of digging to understand, I&amp;rsquo;d like it to take you twenty minutes of reading.&lt;/p></description><content:encoded><![CDATA[<p>Hi, I&rsquo;m <strong>Vitalii</strong> 👋</p>
<p>I&rsquo;m a Java developer, and I&rsquo;ve been writing backend code for more than 12 years: mostly Spring Boot, microservices, PostgreSQL, Kafka and everything that keeps them running in the cloud.</p>
<p>My favourite hobby is keeping up with what&rsquo;s new: new Java releases, new tools, new trends. Lately that&rsquo;s been a lot of AI tooling like Claude Code, MCP and RAG. Whenever I learn something, I try it out for real, and then write about it here. If something took me a weekend of digging to understand, I&rsquo;d like it to take you twenty minutes of reading.</p>
<h2 id="what-youll-find-here">What you&rsquo;ll find here</h2>
<ul>
<li>How Java actually works under the hood: the memory model, concurrency, builds</li>
<li>PostgreSQL stuff I ran into in production</li>
<li>Spring and Hibernate gotchas</li>
<li>AI tools for developers, tried hands-on</li>
<li>Notes on how teams and projects really work</li>
</ul>
<h2 id="when-im-not-writing-backend-code">When I&rsquo;m not writing backend code</h2>
<p>I make games! Game development is my other hobby. You can check out what I&rsquo;ve built on <a href="https://play.google.com/store/apps/details?id=com.sukachvv">Google Play</a>.</p>
<h2 id="say-hi">Say hi</h2>
<ul>
<li>Medium: <a href="https://medium.com/@sukach.v.v">@sukach.v.v</a></li>
<li>GitHub: <a href="https://github.com/SukachVitally">SukachVitally</a></li>
<li>LinkedIn: <a href="https://www.linkedin.com/in/vitalii-sukach/">vitalii-sukach</a></li>
</ul>
]]></content:encoded></item></channel></rss>