<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Heap &amp; Wire - Medium]]></title>
        <description><![CDATA[Deep dives into Java, Spring and modern backend architecture - Medium]]></description>
        <link>https://medium.com/heap-wire?source=rss----7213f8fd7d50---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Heap &amp;amp; Wire - Medium</title>
            <link>https://medium.com/heap-wire?source=rss----7213f8fd7d50---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 23:12:47 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/heap-wire" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Spring AI 2.0 Without the Hype: An HTTP Client That Costs Money and Sometimes Lies]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/spring-ai-2-0-without-the-hype-an-http-client-that-costs-money-and-sometimes-lies-033fe0f1f286?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*DVg1fXUQB75VT92MnO8krg.png" width="1672"></a></p><p class="medium-feed-snippet">Strip the magic from an LLM call. What is left?</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/spring-ai-2-0-without-the-hype-an-http-client-that-costs-money-and-sometimes-lies-033fe0f1f286?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/spring-ai-2-0-without-the-hype-an-http-client-that-costs-money-and-sometimes-lies-033fe0f1f286?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/033fe0f1f286</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[spring-ai]]></category>
            <category><![CDATA[spring-boot]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Thu, 05 Nov 2026 02:31:01 GMT</pubDate>
            <atom:updated>2026-11-05T02:31:01.884Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Virtual Threads in Spring Boot: One Property, Five Surprises]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/virtual-threads-in-spring-boot-one-property-five-surprises-91b3b145294e?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1671/1*oJrbc6MluZpH5rzypOOxow.png" width="1671"></a></p><p class="medium-feed-snippet">That one line can let your service hold 5,000 requests in flight where it used to hold 200. It can also remove the only thing protecting&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/virtual-threads-in-spring-boot-one-property-five-surprises-91b3b145294e?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/virtual-threads-in-spring-boot-one-property-five-surprises-91b3b145294e?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/91b3b145294e</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[virtual-threads]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 02:31:02 GMT</pubDate>
            <atom:updated>2026-11-03T02:31:01.906Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[What reading the ArrayList source code taught me about API Design]]></title>
            <link>https://medium.com/heap-wire/what-reading-the-arraylist-source-code-taught-me-about-api-design-1b79408d3bac?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/1b79408d3bac</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 02:31:02 GMT</pubDate>
            <atom:updated>2026-11-02T02:31:01.862Z</atom:updated>
            <content:encoded><![CDATA[<p>You spend years writing:</p><pre>List&lt;Customer&gt; customers = new ArrayList&lt;&gt;();</pre><p>You know what ArrayList does.</p><p>You know it is backed by an array. You know get() is fast.</p><p>You know inserting near the front is not.</p><p>You know it grows.</p><p>Then one day you open ArrayList.java.</p><p>And there it is.</p><p>Not the tiny collection class you imagined.</p><p>A real piece of production machinery.</p><p>Growth policies. Range checks. Bulk operations. Iterator bookkeeping. Compatibility constraints. Serialization support. Specialised fast paths. System.arraycopy. JVM-aware method sizing.</p><p>The current OpenJDK implementation is more than 1,600 lines of source, even though the fundamental data structure can be described in a sentence: <strong>a resizable array</strong>.</p><p>And that was the interesting part for me.</p><p>I did not come away thinking:</p><blockquote><em>“I should never write an </em><em>ArrayList.”</em></blockquote><p>I came away thinking:</p><blockquote><strong><em>“This is what a mature API looks like after millions of programmers have depended on it.”</em></strong></blockquote><p>The source code taught me that API design is less about inventing clever abstractions and more about making a small set of promises, preserving them for years, and hiding the ugly machinery required to keep those promises.</p><h3>Start with the public promise</h3><p>The public API of ArrayList is wonderfully boring:</p><pre>List&lt;String&gt; names = new ArrayList&lt;&gt;();<br>names.add(&quot;Ada&quot;);<br>names.add(&quot;Grace&quot;);<br>String first = names.get(0);</pre><p>Nobody writing this code needs to know about:</p><pre>Object[] elementData;<br>int size;<br>int modCount;<br>ArraysSupport.newLength(...);<br>System.arraycopy(...);</pre><p>That is the first API-design lesson:</p><blockquote><strong><em>A good abstraction lets users depend on behaviour without depending on implementation.</em></strong></blockquote><p>The current JDK documentation promises constant-time size, isEmpty, get, set, iterator-related operations and amortized constant-time append. It deliberately does <strong>not</strong> promise a particular growth factor</p><p>That distinction is incredibly important.</p><p>The implementation has freedom.</p><p>The caller has a contract.</p><p>Those are not the same thing.</p><h3>The first surprise: the growth policy is deliberately not your business</h3><p>Every Java developer eventually learns the story:</p><blockquote><em>“An </em><em>ArrayList grows by 50%.”</em></blockquote><p>That is a useful implementation detail.</p><p>It is not the API contract.</p><p>The current source delegates growth to:</p><pre>ArraysSupport.newLength(<br>        oldCapacity,<br>        minCapacity - oldCapacity,<br>        oldCapacity &gt;&gt; 1<br>);</pre><p>The implementation uses the old capacity as the preferred growth basis while preserving the API’s broader amortized-performance guarantee.</p><p>That taught me something subtle.</p><p>If the JDK had documented:</p><pre>capacity always grows by exactly 50%</pre><p>then an implementation optimisation could become a compatibility problem.</p><p>Instead, the API says, roughly:</p><pre>adding elements is amortized O(1)</pre><p>and keeps the exact strategy private.</p><p>That gives future JDK maintainers room to change the machinery.</p><p>This is a huge API-design principle:</p><blockquote><strong><em>Promise the property users need, not the implementation detail you happen to have today.</em></strong></blockquote><h3>Good APIs leave room for better implementations</h3><p>Imagine an API like this:</p><pre>interface FastList&lt;E&gt; {<br>    /**<br>     * Capacity grows by exactly 1.5x.<br>     */<br>    void add(E element);<br>}</pre><p>Congratulations.</p><p>You just documented your implementation.</p><p>Now someone discovers a better growth strategy.</p><p>You cannot change it without worrying about somebody depending on the behaviour they found in your documentation.</p><p>ArrayList avoids this trap.</p><p>The caller needs:</p><pre>append is efficient</pre><p>not:</p><pre>the internal array grows from 1000 to 1500</pre><p>This sounds obvious.</p><p>Thousands of APIs still get this wrong.</p><h3>get() is almost comically simple</h3><p>The current source contains a method with the essential shape:</p><pre>public E get(int index) {<br>    Objects.checkIndex(index, size);<br>    return elementData(index);<br>}</pre><p>and the helper eventually reads:</p><pre>return (E) elementData[index];</pre><p>That’s it.</p><p>There is no linked-list traversal.</p><p>No iterator.</p><p>No search.</p><p>Just:</p><pre>check<br>index<br>read</pre><p>This is the payoff for choosing the underlying representation correctly.</p><p>The public contract says random access is efficient.</p><p>The implementation delivers by making the representation match the access pattern.</p><p>ArrayList also implements:</p><pre>RandomAccess</pre><p>which communicates that positional access is efficient and lets library code make informed choices about iteration strategies.</p><p>That’s another API lesson:</p><blockquote><strong><em>Interfaces can communicate performance characteristics without exposing the implementation.</em></strong></blockquote><h3>set() shows another good design choice</h3><p>The current implementation is roughly:</p><pre>public E set(int index, E element) {<br>    Objects.checkIndex(index, size);<br>    E oldValue = elementData(index);<br>    elementData[index] = element;<br>    return oldValue;<br>}</pre><p>Notice what the API tells you.</p><p>set() returns the old value.</p><p>That is useful because it makes replacement a single operation:</p><pre>String previous = names.set(0, &quot;Katherine&quot;);</pre><p>Instead of requiring:</p><pre>String previous = names.get(0);<br>names.set(0, &quot;Katherine&quot;);</pre><p>The API captures an operation that callers genuinely care about.</p><p>That is a design habit worth stealing:</p><blockquote><strong><em>Design methods around useful state transitions, not merely around the fields inside your implementation.</em></strong></blockquote><h3>Then add() gets interesting</h3><p>Appending looks trivial:</p><pre>public boolean add(E e) {<br>    modCount++;<br>    add(e, elementData, size);<br>    return true;<br>}</pre><p>The actual insertion is pushed into a helper:</p><pre>private void add(E e, Object[] elementData, int s) {<br>    if (s == elementData.length)<br>        elementData = grow();<br>   <br>    elementData[s] = e;<br>    size = s + 1;<br>}</pre><p>That helper contains an especially revealing comment: it is split out to keep the bytecode for add(E) small enough to help C1 inlining</p><p>That is the kind of detail you don’t put into your public API.</p><p>But it is exactly the kind of detail a production implementation may need.</p><p>The user sees:</p><pre>list.add(value);</pre><p>The implementation worries about:</p><pre>array capacity<br>copying<br>size<br>modification counts<br>JIT compilation</pre><p>This is abstraction working properly.</p><h3>A public method can be tiny while the implementation is sophisticated</h3><p>This is one of my favourite lessons from the source.</p><p>Good API:</p><pre>boolean add(E e)</pre><p>Underlying machinery:</p><pre>validate<br>check capacity<br>possibly allocate<br>copy<br>insert<br>update size<br>update modification count<br>return</pre><p>You do not make users manage that sequence.</p><p>They call one method.</p><p>That’s not “hiding complexity” in the bad sense.</p><p>It is <strong>encapsulating complexity</strong>.</p><p>The distinction matters.</p><p>Bad abstraction hides information the caller needs.</p><p>Good abstraction hides machinery the caller should never have needed.</p><h3>add(index, element) shows the real cost model</h3><p>Insert at the end:</p><pre>list.add(element);</pre><p>is usually cheap.</p><p>Insert in the middle:</p><pre>list.add(10, element);</pre><p>requires the array contents after that position to move.</p><p>The current source uses:</p><pre>System.arraycopy(<br>        elementData,<br>        index,<br>        elementData,<br>        index + 1,<br>        s - index<br>);</pre><p>before storing the new element.</p><p>That is why the API documentation carefully distinguishes operation costs instead of pretending every list operation is O(1). The JDK explicitly describes the random-access operations as constant time, append as amortized constant time, and other operations as generally linear.</p><p>There is an API lesson here too:</p><blockquote><strong><em>Good abstractions don’t hide performance reality; they describe it at the right level.</em></strong></blockquote><p>List gives you the abstraction.</p><p>ArrayList gives you a documented performance profile.</p><p>You choose the implementation based on the workload.</p><h3>The List interface is doing more work than most developers realise</h3><p>When you write:</p><pre>List&lt;Order&gt; orders = new ArrayList&lt;&gt;();</pre><p>you are making two design choices:</p><pre>I want List semantics.<br>I want ArrayList performance characteristics.</pre><p>The variable is typed to the abstraction.</p><p>The object provides the implementation.</p><p>That makes replacement possible:</p><pre>List&lt;Order&gt; orders = new LinkedList&lt;&gt;();</pre><p>or another implementation when appropriate.</p><p>The beauty is that the caller depends primarily on:</p><pre>List contract</pre><p>not:</p><pre>Object[] layout</pre><p>This is a classic example of programming to an interface.</p><p>But reading the source makes the reason more concrete.</p><p>The interface is the promise.</p><p>The class is the machinery.</p><h3>remove() is where the implementation gets very practical</h3><p>The source’s removal logic essentially does:</p><pre>int newSize = size - 1;<br>if (newSize &gt; index)<br>    System.arraycopy(<br>            elementData,<br>            index + 1,<br>            elementData,<br>            index,<br>            newSize - index<br>    );<br>elementData[size = newSize] = null;</pre><p>That final:</p><pre>= null</pre><p>looks almost insignificant.</p><p>It isn’t.</p><p>It clears the obsolete reference so the removed object is no longer kept alive by the backing array.</p><p>This is a wonderful example of implementation detail serving an externally invisible property:</p><pre>removed objects should become eligible for garbage collection</pre><p>The API user does not know or care about the backing-array slot.</p><p>The implementation absolutely has to care.</p><h3>clear() tells the same story</h3><p>The current implementation resets the size and clears the old element references:</p><pre>modCount++;<br><br>final Object[] es = elementData;<br>for (int to = size, i = size = 0; i &lt; to; i++)<br>    es[i] = null;</pre><p>Again:</p><pre>logical size</pre><p>and:</p><pre>physical storage</pre><p>are separate concepts.</p><p>The list can be logically empty while still retaining its capacity.</p><p>That is why clear() and trimToSize() are different operations.</p><p>clear() changes the contents.</p><p>trimToSize() changes the storage footprint.</p><p>The distinction is excellent API design.</p><p>Different operations answer different user needs.</p><h3>ensureCapacity() is a beautiful example of exposing a useful knob</h3><p>Suppose you know:</p><pre>var orders = new ArrayList&lt;Order&gt;();</pre><p>will receive 100,000 elements.</p><p>You can say:</p><pre>orders.ensureCapacity(100_000);</pre><p>The current implementation can expand the backing storage ahead of time, reducing incremental reallocations when a large number of elements is about to be added. The JDK documents exactly this use case.</p><p>Notice what the API exposes:</p><pre>ensureCapacity(...)</pre><p>It does <strong>not</strong> expose:</p><pre>getInternalArray()<br>setCapacity(...)</pre><p>The user gets a useful performance control.</p><p>The internal representation remains private.</p><p>That’s a very strong pattern:</p><blockquote><strong><em>Expose tuning controls at the level of user intent, not at the level of implementation state.</em></strong></blockquote><h3>The source is full of defensive boundaries</h3><p>Good library code assumes callers can be wrong.</p><p>For example:</p><pre>Objects.checkIndex(index, size);</pre><p>is central to positional access.</p><p>It is tempting to think:</p><blockquote><em>“Why not just let the array throw?”</em></blockquote><p>Because the collection’s API should control its own contract.</p><p>The implementation wants errors that mean :</p><pre>index is invalid for this List</pre><p>rather than allowing the underlying storage mechanism to define the entire user experience.</p><p>This pattern appears everywhere in good APIs:</p><pre>validate<br>establish invariant<br>perform operation</pre><p>The abstraction owns its boundary conditions.</p><h3>Fail-fast is a debugging feature, not a synchronization mechanism</h3><p>This distinction gets misunderstood constantly.</p><p>People see:</p><pre>ConcurrentModificationException</pre><p>and imagine:</p><pre>ArrayList detected concurrency and stopped it.</pre><p>No.</p><p>ArrayList itself is not synchronized.</p><p>The JDK documentation says that if multiple threads access an ArrayList and at least one structurally modifies it, external synchronization is required. It also explicitly warns that fail-fast behaviour is best effort.</p><p>That is a useful API-design pattern:</p><pre>detect misuse</pre><p>is not the same as:</p><pre>guarantee safety</pre><p>Libraries should make that distinction painfully clear.</p><h3>What source code reveals that documentation cannot</h3><p>Documentation tells you:</p><pre>what the API promises.</pre><p>Source code tells you:</p><pre>how seriously those promises are engineered.</pre><p>Reading ArrayList shows you a class that is constantly balancing:</p><pre>correctness<br>performance<br>memory use<br>backward compatibility<br>JVM behaviour<br>API semantics</pre><p>For example:</p><pre>Objects.checkIndex(...)</pre><p>exists for clean boundary checking.</p><pre>System.arraycopy(...)</pre><p>exists for efficient movement.</p><pre>elementData[...] = null</pre><p>exists to release obsolete references.</p><pre>modCount</pre><p>supports fail-fast diagnostics.</p><pre>ArraysSupport.newLength(...)</pre><p>handles growth safely.</p><p>These details are not random implementation trivia.</p><p>They are evidence of the contract being defended from multiple directions.</p><h3>What I would steal for my own APIs</h3><p>After reading ArrayList, my API checklist became much simpler.</p><h3>Promise behaviour, not internals</h3><p>Prefer:</p><pre>amortized constant-time append</pre><p>over:</p><pre>we grow by 50%</pre><h3>Name user intent</h3><p>Prefer:</p><pre>ensureCapacity(...)</pre><p>over:</p><pre>resizeInternalArray(...)</pre><h3>Document performance where it matters</h3><p>Users need to know that:</p><pre>get()</pre><p>is cheap while middle insertion is not.</p><h3>Protect ownership boundaries</h3><p>Copies and views are different contracts.</p><p>Say which one you return.</p><h3>Make failure semantics explicit</h3><p>Don’t rely on accidental exceptions from implementation details.</p><h3>Treat compatibility as a requirement</h3><p>Your API will outlive your current implementation.</p><h3>Optimise behind the abstraction</h3><p>Special-case common paths internally.</p><p>Don’t force every caller to learn the tricks.</p><h3>Keep escape hatches narrow</h3><p>A tuning hook can be valuable.</p><p>An API that exposes the entire implementation usually becomes unchangeable.</p><h3>The final lesson</h3><p>I opened ArrayList.java expecting to learn more about arrays.</p><p>I learned more about APIs.</p><p>The array is the easy part.</p><p>The hard part is everything around it:</p><pre>How do you grow without making append expensive?<br>How do you remove without retaining dead references?<br>How do you report invalid indexes?<br>How do you detect accidental structural modification?<br>How do you preserve decades of behaviour?<br>How do you expose useful performance controls without exposing representation?<br>How do you optimise common cases without changing the contract?<br>How do you leave enough implementation freedom for the next ten years?</pre><p>That is what mature library code looks like.</p><p>The public surface stays boring:</p><pre>add()<br>get()<br>set()<br>remove()<br>size()</pre><p>The implementation underneath is allowed to be absolutely feral.</p><p>And that may be the best API-design lesson of all:</p><blockquote><strong><em>Make the public API simple enough that users don’t need to care how clever you had to be underneath it.</em></strong></blockquote><p>ArrayList is a resizable array.</p><p>That’s the concept.</p><p>The source code is everything it takes to make that concept remain trustworthy after billions of calls, thousands of JDK releases, and an entire ecosystem of developers who have never once thought about elementData.</p><p>That’s not accidental simplicity.</p><p>That’s engineered simplicity.</p><p>And after reading the source, I think that’s what good API design actually is</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b79408d3bac" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heap-wire/what-reading-the-arraylist-source-code-taught-me-about-api-design-1b79408d3bac">What reading the ArrayList source code taught me about API Design</a> was originally published in <a href="https://medium.com/heap-wire">Heap &amp; Wire</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ORMs Are a Response to Java Not Having Structural Types]]></title>
            <link>https://medium.com/heap-wire/orms-are-a-response-to-java-not-having-structural-types-32d65cc1d01a?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/32d65cc1d01a</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[functional-programming]]></category>
            <category><![CDATA[orm]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 02:31:02 GMT</pubDate>
            <atom:updated>2026-10-30T02:31:01.800Z</atom:updated>
            <content:encoded><![CDATA[<p>There is a peculiar ritual in enterprise Java.</p><p>You have a database table:</p><pre>select id, name, email<br>from customer;</pre><p>You want three values.So somebody builds:</p><pre>@Entity<br>public class Customer {<br>    @Id<br>    private Long id;<br><br>    private String name;<br>    private String email;<br>    // getters<br>    // setters<br>    // constructors<br>    // equals<br>    // hashCode<br>    // maybe lazy-loading surprises<br>}</pre><p>Then a repository. Then a mapper. Then a DTO.</p><p>Then a service.</p><p>Then perhaps another DTO because the endpoint does not actually need the database entity.</p><p>Then a query projection because loading the whole entity was wasteful.</p><p>Then a framework-generated proxy because apparently we were not suffering enough.</p><p>And eventually someone asks the dangerous question:</p><blockquote><em>“Why do I need all of this just to get three fields?”</em></blockquote><p>That question leads somewhere interesting.</p><p>Java is nominally typed. A type is primarily identified by a declared type name and its relationships, not simply by the shape of the values it contains.</p><p>A language with structural types can often say something closer to:</p><blockquote><em>“I need any value that has an </em><em>id, a </em><em>name, and an </em><em>email.”</em></blockquote><p>Java generally says:</p><blockquote><em>“I need a </em><em>Customer.”</em></blockquote><p>That difference looks small.</p><p>It is not.</p><p>It shapes how we build DTOs, projections, mappers, repositories, serializers, adapters, and eventually ORMs.</p><p>Now, to be precise, ORMs did <strong>not</strong> appear only because Java lacks structural types. ORMs are primarily a response to the object-relational impedance mismatch: objects, classes, identity, inheritance, references, and collections do not map neatly onto tables, rows, foreign keys, and joins.</p><p>But Java’s nominal, class-centric model makes one part of that mismatch particularly expensive:</p><p><strong>the shape of the data is rarely enough. The type name has to line up too.</strong></p><p>And that is where this gets fun.</p><h3>The tiny problem hiding inside a giant ORM stack</h3><p>Suppose the database contains:</p><pre>customer<br>--------------------------------<br>id | name | email | status | address_id | created_at | ...</pre><p>But your screen only needs:</p><pre>id<br>name<br>email</pre><p>In a structurally typed language, you might imagine working with a shape:</p><pre>{<br>    id: Long,<br>    name: String,<br>    email: String<br>}</pre><p>You do not necessarily care what the original domain class is called.</p><p>In Java, the natural representation is a named type:</p><pre>record CustomerSummary(<br>        long id,<br>        String name,<br>        String email<br>) {}</pre><p>That is perfectly reasonable.</p><p>But now we have introduced a new nominal type just to represent a particular shape.</p><p>Then we need to construct it.</p><p>Then we need to map to it.</p><p>Then the query needs to know how to populate it.</p><p>That is the tiny seam where frameworks start multiplying.</p><h3>Java asks “what type?” before “what shape?”</h3><p>Consider this method:</p><pre>void sendEmail(EmailContact contact) {<br>    ...<br>}</pre><p>The compiler expects:</p><pre>EmailContact</pre><p>It does not care that another record has exactly the same members:</p><pre>record CustomerContact(<br>        String name,<br>        String email<br>) {}</pre><p>These two shapes may be identical, but they are different types.</p><p>You cannot casually substitute one for the other.</p><p>That is a feature, not a bug.</p><p>Nominal typing gives you identity, clear contracts, refactoring support, and a useful way to prevent accidental interchangeability.</p><p>But it also means that when the real problem is simply:</p><pre>I need these fields</pre><p>you often end up inventing another named type.</p><p>And large enterprise systems contain a lot of “I need these fields” problems.</p><h3>This is why DTOs multiply</h3><p>Imagine an endpoint returning:</p><pre>{<br>  &quot;id&quot;: 42,<br>  &quot;name&quot;: &quot;Ada&quot;<br>}</pre><p>The database entity has:</p><pre>@Entity<br>class Customer {<br>    Long id;<br>    String name;<br>    String email;<br>    String status;<br>    Address address;<br>    Instant createdAt;<br>    Instant updatedAt;<br>    // ...<br>}</pre><p>You probably do not want to expose the whole entity.</p><p>So you create:</p><pre>record CustomerResponse(<br>        Long id,<br>        String name<br>) {}</pre><p>Then somewhere:</p><pre>new CustomerResponse(<br>        customer.getId(),<br>        customer.getName()<br>);</pre><p>Now imagine twenty endpoints.</p><p>The business logic is not necessarily complicated.</p><p>The mapping is.</p><p>This is the same underlying pressure that gives us:</p><pre>entities<br>DTOs<br>view models<br>projection interfaces<br>mapper classes<br>record projections<br>serialization metadata</pre><p>The more frequently your application asks for slightly different shapes, the more painful nominal typing can become.</p><h3>ORMs are partially an answer to this pressure</h3><p>Here is the classic ORM promise:</p><pre>row ↔ object<br>foreign key ↔ reference<br>join table ↔ collection relationship<br>column ↔ field/property</pre><p>The framework says:</p><blockquote><em>“Don’t write all the boring translation code. Tell me your model and I will bridge the worlds.”</em></blockquote><p>That is incredibly useful.</p><p>Hibernate and Jakarta Persistence let you define mappings declaratively:</p><pre>@Entity<br>class Customer {<br><br>    @Id<br>    private Long id;<br>    private String name;<br>    private String email;<br>}</pre><p>Then code can work in terms of:</p><pre>Customer customer = entityManager.find(Customer.class, id);</pre><p>instead of manually writing:</p><pre>execute SQL<br>read ResultSet<br>construct object<br>convert types<br>manage identity<br>track relationships<br>handle lifecycle</pre><p>The ORM absorbs a huge amount of object-relational plumbing.</p><p>That is the good part.</p><p>The cost is that the framework is now trying to reconcile two different type systems and two different notions of structure.</p><h3>The database is already structurally flexible</h3><p>This is the part that makes the comparison interesting.</p><p>A SQL query can return a shape without inventing a new nominal domain type.</p><p>For example:</p><pre>select<br>    c.id,<br>    c.name,<br>    c.email<br>from customer c;</pre><p>The result shape is effectively:</p><pre>(Long, String, String)</pre><p>or, conceptually:</p><pre>{id: Long, name: String, email: String}</pre><p>SQL is very comfortable with ad hoc result shapes.</p><p>You can join tables and create a new projection in one query:</p><pre>select<br>    c.id,<br>    c.name,<br>    o.total<br>from customer c<br>join orders o on o.customer_id = c.id;</pre><p>That shape did not exist before you wrote the query.</p><p>The database does not need a class called:</p><pre>CustomerOrderSummary</pre><p>before it can return the row.</p><p>Java generally does.</p><p>Or at least, Java wants some declared representation if the result is going to participate in a strongly typed API.</p><p>And now we have a very productive mismatch.</p><h3>Java 16+ quietly improved this story with records</h3><p>Records reduce the cost of creating named data shapes:</p><pre>record CustomerName(long id, String name) {}</pre><p>That is a big improvement over writing:</p><pre>final class CustomerName {<br>    private final long id;<br>    private final String name;<br><br>    CustomerName(long id, String name) {<br>        this.id = id;<br>        this.name = name;<br>    }<br>    long id() { return id; }<br>    String name() { return name; }<br>    // equals<br>    // hashCode<br>    // toString<br>}</pre><p>Records do not make Java structurally typed.</p><p>But they reduce the cost of saying:</p><blockquote><em>“Here is another immutable data shape.”</em></blockquote><p>That turns out to matter a lot for database projections, API models, and intermediate transformations.</p><h3>Record construction still needs a bridge from SQL</h3><p>Suppose:</p><pre>record CustomerSummary(<br>        long id,<br>        String name,<br>        String email<br>) {}</pre><p>You can write SQL that returns exactly those columns.</p><p>But something still needs to connect:</p><pre>SQL result</pre><p>to:</p><pre>CustomerSummary</pre><p>This is where ORMs, JDBC mappers, row mappers, annotation processors, reflection, and generated code all show up.</p><p>The shape exists in one world.</p><p>The nominal Java type exists in another.</p><p>The mapping layer is the translator.</p><p>A large part of modern Java persistence is essentially a sophisticated answer to:</p><blockquote><em>“How do I turn this runtime shape into that compile-time named type?”</em></blockquote><h3>Projection APIs are basically controlled escape hatches</h3><p>JPA-style projections make this relationship explicit.</p><p>For example, you might define:</p><pre>public interface CustomerNameOnly {<br>    Long getId();<br>    String getName();<br>}</pre><p>and ask the persistence framework for that projection.</p><p>Conceptually, you are saying:</p><blockquote><em>“I don’t actually need a </em><em>Customer. I need something with these accessors.”</em></blockquote><p>That is much closer to a structural notion.</p><p>And notice what the framework is doing.</p><p>It is using runtime machinery to simulate flexibility that the static type system does not naturally provide.</p><p>The framework can look at:</p><pre>getId()<br>getName()</pre><p>and adapt a row-backed representation to an interface.</p><p>Java does not need structural typing in the language itself because the framework builds a runtime layer that approximates the useful part.</p><p>That’s a recurring pattern in the Java ecosystem.</p><h3>Dynamic proxies are structural adapters in disguise</h3><p>Suppose a framework sees:</p><pre>interface NameView {<br>    String getName();<br>}</pre><p>and wants to create an implementation without writing:</p><pre>class NameViewImpl implements NameView {<br>    ...<br>}</pre><p>It can generate a proxy.</p><p>Very roughly:</p><pre>NameView view = (NameView) Proxy.newProxyInstance(<br>        NameView.class.getClassLoader(),<br>        new Class&lt;?&gt;[] { NameView.class },<br>        (proxy, method, args) -&gt; {<br>            return row.get(method.getName());<br>        }<br>);</pre><p>Real frameworks are much more sophisticated than this toy example.</p><p>But the idea is revealing.</p><p>You define a shape as an interface.</p><p>The framework supplies the runtime implementation.</p><p>That is very close to saying:</p><blockquote><em>“I don’t care what the concrete object is. I care that it has these members.”</em></blockquote><p>Again, structural typing is not actually happening.</p><p>Nominal typing is still in charge because the proxy must implement a named interface.</p><p>But the runtime framework is bending the system toward shape-based use.</p><h3>This is where ORMs start doing too much</h3><p>The original promise of an ORM is attractive:</p><pre>Customer.class<br>    ↕<br>customer table</pre><p>But real systems rarely stop there.</p><p>They have:</p><pre>partial projections<br>joins<br>aggregates<br>nested objects<br>inheritance<br>lazy relationships<br>fetch plans<br>pagination<br>sorting<br>filters<br>custom SQL</pre><p>At some point the ORM starts inventing ways to represent ad hoc shapes.</p><p>That is why we get things like:</p><pre>projection interfaces<br>constructor expressions<br>criteria APIs<br>query DSLs<br>Tuple<br>Object[]<br>record projections<br>native queries</pre><p>This looks like framework complexity.</p><p>And it is.</p><p>But it is also the framework acknowledging a reality:</p><blockquote><em>relational queries produce arbitrary shapes.</em></blockquote><p>A single entity class cannot represent every useful database result.</p><p>So the ORM grows a language for shapes.</p><h3>A query is often a better data-shape language than a class hierarchy</h3><p>This is one reason SQL is so powerful.</p><p>Consider:</p><pre>select<br>    c.id,<br>    c.name,<br>    count(o.id) as order_count,<br>    coalesce(sum(o.total), 0) as total_spend<br>from customer c<br>left join orders o<br>    on o.customer_id = c.id<br>group by c.id, c.name;</pre><p>The result has a shape:</p><pre>id          : bigint<br>name        : text<br>order_count : bigint<br>total_spend : numeric</pre><p>That shape may exist only for this report.</p><p>It does not deserve to become a giant entity hierarchy.</p><p>A small Java record is a good fit:</p><pre>record CustomerSpend(<br>        long id,<br>        String name,<br>        long orderCount,<br>        BigDecimal totalSpend<br>) {}</pre><p>The record is useful because the data is useful.</p><p>The mistake is assuming every query result must map back to an entity.</p><p>Once you stop doing that, the ORM becomes less magical and more practical.</p><h3>Records let you use Java’s nominal typing without paying the old ceremony tax</h3><p>This is one of the best modern Java improvements for database work.</p><p>You can happily define:</p><pre>record CustomerSpend(<br>        long id,<br>        String name,<br>        long orderCount,<br>        BigDecimal totalSpend<br>) {}</pre><p>and treat it as a value.</p><p>There is no need for:</p><pre>setter explosion<br>mutable DTOs<br>boilerplate equals/hashCode</pre><p>So although Java has not become structurally typed, the cost of saying:</p><blockquote><em>“This query returns this shape.”</em></blockquote><p>has dropped substantially.</p><p>That reduces one of the reasons developers used to reach for increasingly complicated ORM magic.</p><h3>What structural typing would change</h3><p>Imagine Java had a structural record type that let you write something conceptually like:</p><pre>{ id: long, name: String }</pre><p>and say:</p><pre>load anything with this shape</pre><p>Then a query result could potentially flow directly into that shape without defining:</p><pre>CustomerName<br>CustomerNameProjection<br>CustomerNameDto</pre><p>for every tiny variation.</p><p>That could simplify:</p><pre>DTOs<br>projections<br>mappers<br>query results<br>adapter types<br>serialization views</pre><p>But there would be a trade-off.</p><p>Nominal types communicate intent beyond shape.</p><p>For example:</p><pre>record CustomerId(long value) {}<br>record OrderId(long value) {}</pre><p>Both have:</p><pre>long value</pre><p>A structural type system might consider them interchangeable if their shapes matched.</p><p>That could be disastrous.</p><p>Sometimes identical shape does <strong>not</strong> mean identical meaning.</p><p>Nominal typing protects you from that class of mistake.</p><h3>“Same shape” and “same concept” are different things</h3><p>This is the strongest argument for Java’s model.</p><p>These two values are structurally identical:</p><pre>record CustomerId(long value) {}<br>record OrderId(long value) {}</pre><p>But:</p><pre>CustomerId ≠ OrderId</pre><p>And that is exactly what you want.</p><p>If a method accepts:</p><pre>void loadCustomer(CustomerId id) { ... }</pre><p>passing:</p><pre>new OrderId(42)</pre><p>should fail.</p><p>A structural type system can support stronger systems of wrappers or branded types, but nominal identity is a very direct tool for expressing domain meaning.</p><p>So “Java lacks structural types” is not a complaint that should end with:</p><blockquote><em>“Therefore Java should have structural types everywhere.”</em></blockquote><p>The real question is:</p><blockquote><strong><em>Where do we care about shape, and where do we care about identity?</em></strong></blockquote><p>Databases and query projections care heavily about shape.</p><p>Domain boundaries often care heavily about identity.</p><p>ORMs sit in the uncomfortable middle.</p><h3>A better rule for ORM-heavy Java</h3><p>I would use three questions.</p><h3>Does this object have domain identity?</h3><p>If yes, an entity or domain type may be appropriate.</p><h3>Is this object primarily a query result or transport shape?</h3><p>If yes, prefer a small immutable record.</p><h3>Is this relationship really about shape rather than identity?</h3><p>If yes, do not force it into the entity model just because an ORM makes that convenient.</p><p>That one distinction can prevent a surprising amount of ORM complexity.</p><h3>The practical architecture</h3><p>A healthy persistence boundary often looks like:</p><pre>Database<br>                  |<br>                  v<br>             SQL / ORM<br>                  |<br>          +-------+-------+<br>          |               |<br>       Entity         Projection<br>          |               |<br>          v               v<br>     Domain model      Record/value<br>          |               |<br>          +-------+-------+<br>                  |<br>                  v<br>          Application logic<br>                  |<br>                  v<br>              API DTO</pre><p>The important part is that the types are allowed to be different.</p><p>The database does not need to think in terms of your domain classes.</p><p>The API does not need to think in terms of your tables.</p><p>The domain does not need to expose your persistence structure.</p><p>And the mapper is not a sign that the architecture failed.</p><p>It is the explicit seam between models.</p><h3>What ORMs really bought us</h3><p>It is easy to tell a simplistic story:</p><blockquote><em>“Java has no structural types, therefore we got ORMs.”</em></blockquote><p>That is too neat.</p><p>The real history is messier.</p><p>We got ORMs because developers wanted to work with an object model while storing data in relational databases.</p><p>We got ORMs because SQL and objects optimise for different things.</p><p>We got ORMs because manually mapping rows, relationships, identity, and lifecycle is exhausting.</p><p>But Java’s nominal type system adds another layer of friction:</p><pre>SQL can create a shape on demand.<br>Java wants a named type for that shape.</pre><p>That pressure contributes to the huge ecosystem around:</p><pre>DTOs<br>projections<br>mappers<br>reflection<br>code generation<br>query DSLs</pre><p>The ORM is not only mapping objects to tables.</p><p>It is also negotiating between a shape-oriented data system and a name-oriented language.</p><p>That is the more interesting story.</p><h3>And this is why records matter so much</h3><p>Records do not eliminate the object-relational mismatch.</p><p>They do something more achievable and more useful.</p><p>They make tiny, explicit, immutable data shapes cheap.</p><p>That means instead of thinking:</p><blockquote><em>“How do I contort the entity model to represent this query?”</em></blockquote><p>you can think:</p><blockquote><em>“What is the smallest value that represents this result?”</em></blockquote><p>Then write:</p><pre>record OrderSummary(<br>        long orderId,<br>        String customerName,<br>        BigDecimal total<br>) {}</pre><p>That is a very good modern Java answer to a very old problem.</p><p>Not structural typing.</p><p>Just less ceremony around named data.</p><h3>The final rule</h3><p>The interesting lesson is not:</p><blockquote><em>“ORMs are bad.”</em></blockquote><p>They are incredibly useful.</p><p>Nor is it:</p><blockquote><em>“Java should become structurally typed.”</em></blockquote><p>That would solve some problems and create others.</p><p>The more useful lesson is this:</p><blockquote><strong><em>Use nominal types when identity and meaning matter. Use small data shapes when shape is the whole point. Do not force every database result through an entity just because the ORM can.</em></strong></blockquote><p>When the database gives you:</p><pre>{id, name, email}</pre><p>and the use case needs:</p><pre>{id, name, email}</pre><p>create the smallest Java value that says exactly that.</p><p>A record is often enough.</p><p>When the use case needs a long-lived thing with identity, invariants, and behaviour, model that explicitly.</p><p>And when an ORM starts growing another projection API, another mapper layer, another proxy, and another framework abstraction because the query returned five fields that don’t fit your entity…</p><p>stop for a second.</p><p>The framework may not be failing.</p><p>It may simply be paying the tax for a language that asks:</p><blockquote><em>“What is the name of this type?”</em></blockquote><p>when the database already knows the answer to a different question:</p><blockquote><em>“What shape are you asking for?”</em></blockquote><p>That gap is one of the most interesting fault lines in enterprise Java.</p><p>And once you see it, ORMs stop looking like giant magical machines that map objects to tables.</p><p>They start looking like what they really are:</p><p><strong>a very elaborate negotiation between two worlds that agree on data, but disagree about what a type is.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=32d65cc1d01a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heap-wire/orms-are-a-response-to-java-not-having-structural-types-32d65cc1d01a">ORMs Are a Response to Java Not Having Structural Types</a> was originally published in <a href="https://medium.com/heap-wire">Heap &amp; Wire</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Your Monolith Isn’t the Problem, Your Package Structure Is]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/your-monolith-isnt-the-problem-your-package-structure-is-enforcing-architecture-with-spring-7ee502aeb10e?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*4oeVx9bstZDF1sMadcTplw.png" width="1672"></a></p><p class="medium-feed-snippet">Enforcing Architecture With Spring Modulith</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/your-monolith-isnt-the-problem-your-package-structure-is-enforcing-architecture-with-spring-7ee502aeb10e?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/your-monolith-isnt-the-problem-your-package-structure-is-enforcing-architecture-with-spring-7ee502aeb10e?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/7ee502aeb10e</guid>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[spring-modulith]]></category>
            <category><![CDATA[clean-architecture]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Wed, 28 Oct 2026 02:31:01 GMT</pubDate>
            <atom:updated>2026-10-29T21:34:05.900Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Stop Throwing Exceptions From Your Spring Services: Let the Compiler Check Your HTTP Responses]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/stop-throwing-exceptions-from-your-spring-services-let-the-compiler-check-your-http-responses-37b49a732abe?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*t886PGNj6_fC7Kxk1-tfzg.png" width="1672"></a></p><p class="medium-feed-snippet">Open any Spring Boot codebase that&#x2019;s more than two years old and search for NotFoundException.</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/stop-throwing-exceptions-from-your-spring-services-let-the-compiler-check-your-http-responses-37b49a732abe?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/stop-throwing-exceptions-from-your-spring-services-let-the-compiler-check-your-http-responses-37b49a732abe?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/37b49a732abe</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Tue, 27 Oct 2026 02:31:02 GMT</pubDate>
            <atom:updated>2026-10-27T02:31:02.052Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Immutability Is the FP Idea That Paid Off Most in Java]]></title>
            <link>https://medium.com/heap-wire/immutability-is-the-fp-idea-that-paid-off-most-in-java-9895028a2866?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/9895028a2866</guid>
            <category><![CDATA[functional-programming]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 14:31:02 GMT</pubDate>
            <atom:updated>2026-10-25T14:31:01.879Z</atom:updated>
            <content:encoded><![CDATA[<p>There is a strange little victory hiding in Java’s history.</p><p>Java borrowed lambdas from functional programming.</p><p>Then streams. Then Optional. Then records.</p><p>Then sealed types and pattern matching.</p><p>We can spend days arguing about whether Java is becoming a functional language.</p><p>Meanwhile, one of the most valuable functional ideas was sitting quietly in Java code long before any of those features arrived:</p><blockquote><em>Build a value once, then stop changing it.</em></blockquote><p>No special syntax. No pure-language runtime. No effect system. Just ordinary Java classes, constructors, final fields, and a little discipline.</p><p>And when modern Java added records, immutable-style modelling became dramatically easier.</p><p>That matters because immutability solves a surprisingly large part of the problems people reach for functional programming to solve:</p><pre>unexpected state changes<br>aliasing<br>race conditions<br>temporal coupling<br>hard-to-reproduce bugs<br>defensive copying<br>concurrency coordination</pre><p>You mostly have to stop changing things.</p><p>Which sounds trivial.</p><p>Then someone hands you an object graph with 14 nested collections and asks you to update one postcode.</p><p>And suddenly we’re back in reality.</p><p>So this is not a story about “immutability is always easy.” It isn’t.</p><p>It is a story about why it pays off so much that Java developers have been using the idea long before Java acquired the vocabulary to talk about it.</p><h3>The bug is usually not the line you are looking at</h3><p>Consider:</p><pre>class Order {<br>    private String status;<br>    private Customer customer;<br>    <br>    public void setStatus(String status) {<br>        this.status = status;<br>    }<br>    public void setCustomer(Customer customer) {<br>        this.customer = customer;<br>    }<br>}</pre><p>Then somewhere:</p><pre>Order order = repository.find(orderId);<br>emailService.send(order);<br>fraudService.inspect(order);<br>order.setStatus(&quot;PAID&quot;);<br>auditService.record(order);</pre><p>Looks harmless.</p><p>Now imagine emailService keeps the reference:</p><pre>class EmailService {<br>   private Order lastOrder;<br>    void send(Order order) {<br>        this.lastOrder = order;<br>    }<br>}</pre><p>Later:</p><pre>order.setStatus(&quot;CANCELLED&quot;);</pre><p>Now lastOrder is also cancelled.</p><p>Nobody explicitly told EmailService to mutate. Nobody explicitly passed a new value. It just happened because both pieces of code held the same object.</p><p>Welcome to aliasing.</p><p>Mutable state makes references socially complicated.</p><h3>Immutability changes the question</h3><p>Instead of:</p><pre>order.setStatus(&quot;PAID&quot;);</pre><p>you write:</p><pre>Order paidOrder = order.withStatus(&quot;PAID&quot;); //create a new object with the status</pre><p>Now:</p><pre>order      -&gt; status = PENDING<br>paidOrder  -&gt; status = PAID</pre><p>The original value remains unchanged.</p><p>There is no mystery. There is no “who changed this?” investigation. There is no temporal question about which version you were looking at.</p><p>There are simply two values.</p><p>That is the practical magic of immutability.</p><h3>Immutability removes an entire category of questions</h3><p>Mutable code forces you to reason about:</p><pre>who owns this object?<br>who can change it?<br>when can they change it?<br>is someone else holding the same reference?<br>has another thread changed it?<br>do I need to copy it?</pre><p>Once an object is truly immutable, you can pass it around, cache it, return it, share it between threads, use it in a lambda, and put it in a collection without worrying that some unrelated caller will silently rewrite its state.</p><p>The value you received remains the value you received.</p><p>That sounds boring.</p><p>Boring is an underrated concurrency primitive.</p><h3>Java has always been pretty good at immutable values</h3><p>Take:</p><pre>String name = &quot;Alice&quot;;</pre><p>Operations such as:</p><pre>name.toUpperCase();</pre><p>return a new value rather than mutating the existing string.</p><p>The same basic idea appears in types such as:</p><pre>String<br>BigInteger<br>BigDecimal<br>java.time types</pre><p>These types became ubiquitous partly because immutable values are remarkably easy to compose.</p><p>If a value cannot change, you do not need an ownership meeting every time it crosses a method boundary.</p><h3>Mutable objects create temporal coupling</h3><p>Imagine:</p><pre>Account account = new Account();<br>account.setCustomer(customer);<br>account.setCurrency(&quot;GBP&quot;);<br>account.setBalance(new BigDecimal(&quot;1000&quot;));</pre><p>The object exists in several intermediate states.</p><p>A method that receives it after the first setter but before the last setter may see something incomplete.</p><p>The object’s validity depends on history.</p><p>That is temporal coupling.</p><p>With immutable construction:</p><pre>Account account = new Account(<br>        customer,<br>        &quot;GBP&quot;,<br>        new BigDecimal(&quot;1000&quot;)<br>);</pre><p>the value arrives complete.</p><p>You don’t need to remember the sequence of mutator calls that makes it valid.</p><p>The constructor establishes the state. After that, the state stays put.</p><h3>Records made the good path ridiculously short</h3><p>Before records, an immutable value often meant a lot of ceremony:</p><pre>public final class Customer {<br>    private final String id;<br>    private final String name;<br>    private final Address address;<br><br>    public Customer(String id, String name, Address address) {<br>        this.id = id;<br>        this.name = name;<br>        this.address = address;<br>    }<br>    public String id() { return id; }<br>    public String name() { return name; }<br>    public Address address() { return address; }<br>}</pre><p>A record collapses the representation:</p><pre>public record Customer(<br>        String id,<br>        String name,<br>        Address address<br>) {}</pre><p>Records did not invent immutability.</p><p>They made immutable data so cheap to write that more developers actually use it.</p><h3>But a record is not deeply immutable</h3><p>Be careful.</p><pre>record Customer(<br>        String name,<br>        List&lt;String&gt; roles<br>) {}</pre><p>The record component is final, but the referenced list can still be mutable.</p><p>You can do:</p><pre>customer.roles().add(&quot;ADMIN&quot;);</pre><p>if the supplied list is mutable.</p><p>A safer boundary is:</p><pre>record Customer(<br>        String name,<br>        List&lt;String&gt; roles<br>) {<br>    public Customer {<br>        roles = List.copyOf(roles);<br>    }<br>}</pre><p>Now the record stores an immutable snapshot of the collection.</p><p>The broader lesson is:</p><blockquote><em>Immutability is a property of the reachable object graph, not just the top-level reference.</em></blockquote><h3>final is not the same thing as immutable</h3><p>This:</p><pre>final List&lt;String&gt; names = new ArrayList&lt;&gt;();</pre><p>does not make the list immutable.</p><p>It means the variable cannot be pointed at another list.</p><p>This is still legal:</p><pre>names.add(&quot;Ada&quot;);</pre><p>Likewise:</p><pre>final Customer customer = ...;</pre><p>does not make Customer immutable.</p><p>final provides assignment guarantees. Immutability provides state guarantees.</p><p>The second is a larger promise.</p><h3>Builders did not kill immutability</h3><p>A builder can be mutable:</p><pre>Customer customer = Customer.builder()<br>        .name(&quot;Ada&quot;)<br>        .address(address)<br>        .roles(List.of(&quot;USER&quot;))<br>        .build();</pre><p>That is perfectly fine.</p><p>The builder is a temporary construction object.</p><p>The useful boundary is:</p><pre>mutable builder<br>      ↓<br>immutable value<br>      ↓<br>application</pre><p>You do not need every object involved in construction to be immutable.</p><p>You need the published domain value to be immutable.</p><p>Mutation during construction is not the same thing as shared mutable state.</p><h3>Immutability makes concurrency boring</h3><p>Suppose twenty threads all read the same immutable Customer.</p><p>No synchronisation is required merely to protect its fields from mutation because there is no mutation.</p><p>With a mutable object, depending on the access pattern, you may need:</p><pre>locks<br>volatile<br>atomic operations<br>copying<br>thread confinement</pre><p>Immutability removes the shared-state mutation problem instead of making concurrent mutation less dangerous.</p><p>That is why it is such a powerful concurrency technique.</p><h3>Functional pipelines get easier when values are stable</h3><p>Consider:</p><pre>var customers = orders.stream()<br>        .map(Order::customer)<br>        .distinct()<br>        .toList();</pre><p>This is much easier to reason about when Order and Customer are values that do not change underneath the pipeline.</p><p>The functional model is naturally:</p><pre>value → value → value</pre><p>Shared mutable state changes that mental model into:</p><pre>value → who knows what happened → value</pre><p>That is where composition starts leaking.</p><h3>Immutability and value semantics belong together</h3><p>Consider:</p><pre>record Money(<br>        BigDecimal amount,<br>        Currency currency<br>) {}</pre><p>Then:</p><pre>Money price = new Money(<br>        new BigDecimal(&quot;100&quot;),<br>        Currency.getInstance(&quot;GBP&quot;)<br>);<br><br>Money discounted = new Money(<br>        price.amount().multiply(new BigDecimal(&quot;0.9&quot;)),<br>        price.currency()<br>);</pre><p>You can reason about both values independently.</p><p>Modern records make this particularly pleasant because they provide value-oriented equals(), hashCode(), and toString() implementations.</p><p>A good value object does not require you to remember its history.</p><h3>Immutable state reduces defensive copying</h3><p>Mutable APIs often produce code like:</p><pre>List&lt;String&gt; roles() {<br>    return new ArrayList&lt;&gt;(roles);<br>}</pre><p>Why? Because returning the internal list would expose mutable state.</p><p>Then callers make more copies to protect themselves.</p><p>With an immutable representation, the API can safely return the same value:</p><pre>List&lt;String&gt; roles() {<br>    return roles;<br>}</pre><p>No ownership negotiation.</p><p>No documentation saying “please don’t modify this.”</p><p>The value itself enforces the contract.</p><h3>Immutable values are excellent cache values and messages</h3><p>Suppose:</p><pre>Map&lt;String, Customer&gt; customerCache;</pre><p>If Customer is immutable, cached instances can be shared safely from the perspective of their internal state.</p><p>The same principle fits:</p><pre>cache values<br>configuration snapshots<br>event payloads<br>commands<br>DTOs<br>read models</pre><p>An event such as:</p><pre>record OrderPlaced(<br>        String orderId,<br>        String customerId,<br>        Instant occurredAt<br>) {}</pre><p>is naturally a value representing what happened at a point in time.</p><p>History is a particularly bad place for setters.</p><h3>Immutability makes tests easier</h3><p>Mutable test fixtures tend to require a sequence of setup calls:</p><pre>Customer customer = new Customer();<br>customer.setName(&quot;Ada&quot;);<br>customer.setCountry(&quot;UK&quot;);<br>customer.setActive(true);</pre><p>Now test state depends on setup order.</p><p>Immutable construction makes the fixture explicit:</p><pre>Customer customer =<br>        new Customer(&quot;Ada&quot;, &quot;UK&quot;, true);</pre><p>And variants become values:</p><pre>Customer inactive =<br>        customer.withActive(false);</pre><p>That is very close to the functional style of:</p><pre>start with a value<br>derive another value</pre><h3>Immutability can expose missing domain operations</h3><p>Suppose you have:</p><pre>order.setStatus(PAID);<br>order.setApprovedBy(user);<br>order.setApprovedAt(now);<br>order.setApprovedReason(reason);</pre><p>These are not necessarily four independent field updates.</p><p>They may represent one business operation:</p><pre>Order approved =<br>        order.approve(user, now, reason);</pre><p>The immutable design forces a useful question:</p><blockquote><em>What does this state transition actually mean?</em></blockquote><p>That often improves the domain model.</p><h3>The awkward part: nested immutable updates</h3><p>Now for the bit everybody eventually hits.</p><pre>record Customer(<br>        String name,<br>        Address address<br>) {}<br><br>record Address(<br>        String city,<br>        Postcode postcode<br>) {}<br>record Postcode(<br>        String value<br>) {}</pre><p>You want to change:</p><pre>Customer → Address → Postcode → value</pre><p>With mutation:</p><pre>customer.getAddress()<br>        .getPostcode()<br>        .setValue(&quot;M1 1AA&quot;);</pre><p>With immutable values:</p><pre>var updated = new Customer(<br>        customer.name(),<br>        new Address(<br>                customer.address().city(),<br>                new Postcode(&quot;M1 1AA&quot;)<br>        )<br>);</pre><p>Correct. Explicit. A little pyramid-shaped.</p><p>This is where developers begin missing setters.</p><p>And this is where builders, withX() methods, copy methods, persistent data structures, and lenses earn their keep.</p><p>Immutability solved the state problem. It did not promise to solve the syntax problem.</p><h3>withX() methods are a practical middle ground</h3><p>For small immutable types:</p><pre>record Customer(String id,<br>        String name,<br>        Address address) {<br>    <br>    Customer withName(String name) {<br>        return new Customer(id, name, address);<br>    }<br><br>    Customer withAddress(Address address) {<br>        return new Customer(id, name, address);<br>    }<br>}</pre><p>Then:</p><pre>var updated = customer.withName(&quot;Grace&quot;);</pre><p>This keeps the update readable without introducing a framework.</p><p>For deeper object graphs, though, nested reconstruction can become ugly. That is a real cost.</p><h3>This is where lenses enter the conversation</h3><p>A lens is a reusable description of how to focus on one part of an immutable structure.</p><p>Conceptually:</p><pre>Lens&lt;Customer, String&gt; postcode =<br>        addressLens<br>                .andThen(postcodeLens)<br>                .andThen(valueLens);</pre><p>Then:</p><pre>Customer updated =<br>        postcode.set(customer, &quot;M1 1AA&quot;);</pre><p>The distinction is important:</p><pre>immutability<br>    = design principle<br><br>lens<br>    = tool for convenient immutable updates</pre><p>You do not need lenses to benefit from immutability.</p><p>You reach for them when structural updates become repetitive enough to hurt.</p><h3>Not every object needs to be immutable</h3><p>This is where dogma gets expensive.</p><p>Suppose you have a local accumulator:</p><pre>var result = new ArrayList&lt;Result&gt;();<br><br>for (Input input : inputs) {<br>    if (!isValid(input)) {<br>        continue;<br>    }<br>    result.add(transform(input));<br>}</pre><p>There is local mutation.</p><p>That is not automatically a design failure.</p><p>The useful distinction is:</p><pre>shared mutable state</pre><p>versus:</p><pre>local mutation with clear ownership</pre><p>A local variable that nobody else can see is often easy to reason about.</p><p>A mutable object shared between services and threads is much harder.</p><h3>Ownership is a better rule than purity</h3><p>A practical rule is:</p><blockquote><em>The more widely an object is shared, the more valuable immutability becomes.</em></blockquote><p>Think about the gradient:</p><pre>local method variable<br>    ↓<br>usually easy to mutate<br><br>request-local state<br>    ↓<br>often safe with clear ownership<br><br>shared service state<br>    ↓<br>prefer immutable or thread-safe state<br><br>cache value<br>    ↓<br>immutability is very useful<br><br>event/message<br>    ↓<br>immutability is usually ideal</pre><p>That is a much more useful rule than “never mutate anything.”</p><h3>Immutability can cost you something</h3><p>Let’s be honest.</p><p>Immutable updates may mean more object creation.</p><p>They may mean more construction code.</p><p>Nested records can be annoying to update.</p><p>Collection boundaries may need List.copyOf() or similar defensive copying.</p><p>Persistent data structures are not built into the standard Java collections in the same way they are central to some functional ecosystems.</p><p>And in a very hot loop, replacing one in-place mutation with several allocations can matter.</p><p>But there is an equally important correction:</p><blockquote><em>“Immutable means slow” is just as simplistic as “allocation is free.”</em></blockquote><p>Modern JVMs are very good at handling short-lived objects, and JIT optimisation can remove some allocations entirely. The right question is still the real workload: measure before changing a clear design for a theoretical performance problem.</p><h3>The compiler gets more useful when state stops changing</h3><p>With mutable code, a debugger often forces you to ask:</p><pre>who last changed this field?<br>when did it change?<br>what thread changed it?<br>was the object shared?</pre><p>With immutable values, inspection is much more local.</p><p>If:</p><pre>Customer customer = ...;</pre><p>is immutable, the debugger can show you the value and you can trust that value to remain stable while you inspect the surrounding code.</p><p>That is a surprisingly large productivity benefit.</p><h3>Functional Java gets easier when the middle is made of values</h3><p>The most useful modern-Java combination is often:</p><pre>records<br>    ↓<br>immutable data<br>    ↓<br>small functions<br>    ↓<br>streams / pattern matching<br>    ↓<br>new immutable data</pre><p>For example:</p><pre>var result = orders.stream()<br>        .filter(Order::isPaid)<br>        .map(this::calculateDiscount)<br>        .map(this::createSummary)<br>        .toList();</pre><p>That pipeline is easy to reason about because each stage can be thought of as a transformation.</p><p>The more those functions mutate shared state, the less that model helps.</p><h3>Immutability is the FP idea Java adopted without needing to become FP</h3><p>This is the part I find most interesting.</p><p>Java did not need a radical new syntax to get enormous value from immutability.</p><p>It needed a design decision:</p><pre>construct once<br>keep state stable<br>return a new value when something changes</pre><p>Then the language gradually made that design cheaper:</p><pre>records<br>pattern matching<br>lambdas<br>streams<br>sealed types</pre><p>These features did not make immutability valuable.</p><p>Immutability made many of those features easier to use safely.</p><h3>The final idea</h3><p>Functional programming is often introduced as a collection of exotic concepts.</p><p>Some of its most valuable ideas are almost aggressively ordinary.</p><p>Don’t mutate the value. Don’t share state unless you need to.</p><p>Pass values around. Create a new value when something changes.</p><p>Make invalid states harder to represent.</p><p>Keep mutation at the boundary where it is easiest to control.</p><p>That gets you surprisingly far. It just needed to stop asking every object to be a tiny database.</p><p>Because once a value can safely travel through:</p><pre>threads<br>streams<br>caches<br>messages<br>tests<br>services</pre><p>without changing underneath you, an enormous amount of code gets easier.</p><p>That is the part people sometimes miss when they talk about functional programming in Java.</p><p>The biggest win isn’t that the code looks functional.</p><p>It is that the state stops being surprising.</p><p>And after a few years of production debugging, you start to realise that “nothing changed” is one of the most beautiful properties an object can have.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9895028a2866" width="1" height="1" alt=""><hr><p><a href="https://medium.com/heap-wire/immutability-is-the-fp-idea-that-paid-off-most-in-java-9895028a2866">Immutability Is the FP Idea That Paid Off Most in Java</a> was originally published in <a href="https://medium.com/heap-wire">Heap &amp; Wire</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Consistent Hashing From Scratch: How Adding One Server Stopped Moving 90% of Our Data]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/consistent-hashing-from-scratch-how-adding-one-server-stopped-moving-90-of-our-data-30d93816bcdd?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*eor6MuzkLqUReDJ4sniedg.png" width="1672"></a></p><p class="medium-feed-snippet">There is a particular kind of Friday afternoon that starts with good news. Traffic is up, the cache cluster is running warm, and somebody&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/consistent-hashing-from-scratch-how-adding-one-server-stopped-moving-90-of-our-data-30d93816bcdd?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/consistent-hashing-from-scratch-how-adding-one-server-stopped-moving-90-of-our-data-30d93816bcdd?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/30d93816bcdd</guid>
            <category><![CDATA[site-reliability-engineer]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 02:31:02 GMT</pubDate>
            <atom:updated>2026-10-29T21:40:29.109Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Your Health Check Is Taking Down Your Cluster]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/your-health-check-is-taking-down-your-cluster-9db76ccfdb84?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*g0JIRqeeQnACSJUR7T064w.png" width="1672"></a></p><p class="medium-feed-snippet">Here&#x2019;s a thing nobody puts in the architecture diagram: the most dangerous code in your system is probably a twelve-line method called&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/your-health-check-is-taking-down-your-cluster-9db76ccfdb84?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/your-health-check-is-taking-down-your-cluster-9db76ccfdb84?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/9db76ccfdb84</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Sat, 24 Oct 2026 01:31:02 GMT</pubDate>
            <atom:updated>2026-10-24T01:31:01.895Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Spring Boot Doesn’t Need a gRPC Starter. It Needs About 120 Lines and a Little Courage.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/heap-wire/spring-boot-doesnt-need-a-grpc-starter-it-needs-about-120-lines-and-a-little-courage-48a6d4594cf9?source=rss----7213f8fd7d50---4"><img src="https://cdn-images-1.medium.com/max/1672/1*7crWX32Ds3342D753ShOwg.png" width="1672"></a></p><p class="medium-feed-snippet">A gRPC service had started dropping connections during deploys. Not all of them, just enough to make dashboards twitchy and customers&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/heap-wire/spring-boot-doesnt-need-a-grpc-starter-it-needs-about-120-lines-and-a-little-courage-48a6d4594cf9?source=rss----7213f8fd7d50---4">Continue reading on Heap &amp; Wire »</a></p></div>]]></description>
            <link>https://medium.com/heap-wire/spring-boot-doesnt-need-a-grpc-starter-it-needs-about-120-lines-and-a-little-courage-48a6d4594cf9?source=rss----7213f8fd7d50---4</link>
            <guid isPermaLink="false">https://medium.com/p/48a6d4594cf9</guid>
            <category><![CDATA[grpc-java]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[microservices]]></category>
            <dc:creator><![CDATA[Jitin Kayyala]]></dc:creator>
            <pubDate>Fri, 23 Oct 2026 01:31:02 GMT</pubDate>
            <atom:updated>2026-10-23T01:31:01.955Z</atom:updated>
        </item>
    </channel>
</rss>