<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Zero Duplications</title>
  <subtitle>On software engineering, philosophy, and AI.</subtitle>
  <link href="https://zero-duplications.com/en/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://zero-duplications.com/en/"/>
  <updated>2026-09-01T00:00:00Z</updated>
  <id>https://zero-duplications.com/en/</id>
  <author>
    <name>Ola Karlsson</name>
  </author>
  <entry>
    <title>What if we needed fewer frameworks?</title>
    <link href="https://zero-duplications.com/en/posts/2026-09-01-fewer-frameworks-more-java/"/>
    <updated>2026-09-01T00:00:00Z</updated>
    <id>https://zero-duplications.com/en/posts/2026-09-01-fewer-frameworks-more-java/</id>
    <summary>An attempt to reason about when Spring Boot is genuinely needed and when plain Java 25 might be enough — with a suggested order of decisions.</summary>
    <content type="html">  &lt;p class=&quot;lead&quot;&gt;For years, Java development has often meant reaching for a framework or library the moment a new need appears. That&#39;s been rational, and I&#39;ve done the same. But the Java platform has grown, the language has gotten better, and the cost of writing mechanical code by hand has dropped. So I&#39;ve started turning over a question I don&#39;t have a finished answer to: &lt;strong&gt;how much of what we usually add do we still actually need?&lt;/strong&gt;&lt;/p&gt;
  &lt;section class=&quot;section grid-2&quot;&gt;
    &lt;div class=&quot;copy&quot;&gt;
      &lt;p class=&quot;eyebrow&quot;&gt;01 · The idea&lt;/p&gt;
      &lt;h2&gt;This isn&#39;t about Spring being the problem&lt;/h2&gt;
      &lt;p&gt;Spring Boot solves a lot of hard problems, and it solves them well. For public APIs, security, advanced routing, observability, and large integration surfaces, Spring is probably still the cheapest option overall.&lt;/p&gt;
      &lt;p&gt;But Spring has become such a default reach that I suspect we sometimes grab it even when the problem is smaller than the solution. That&#39;s the suspicion I want to examine here.&lt;/p&gt;
      &lt;p&gt;A small internal service might need only a handful of HTTP calls, a database, one outgoing call, and some business logic. Java 25 already has an HTTP client, JDBC, scheduling, records, logging, JFR/JMX, and modern concurrency tools. Virtual threads also make plain, blocking code considerably more attractive for I/O than it was a few years ago.&lt;/p&gt;
      &lt;div class=&quot;quote&quot;&gt;Java hasn&#39;t become a new Spring. But maybe the line for what actually needs a framework has shifted a little?&lt;/div&gt;
    &lt;/div&gt;
    &lt;figure class=&quot;figure&quot;&gt;
      &lt;img src=&quot;https://zero-duplications.com/assets/images/figure-01.webp&quot; alt=&quot;Abstract network diagram where a clear core connects to a limited number of outer systems.&quot; /&gt;
      &lt;figcaption&gt;A goal I find appealing: a clear core and a small number of deliberate boundaries toward the outside world.&lt;/figcaption&gt;
    &lt;/figure&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;02 · The maintenance surface&lt;/p&gt;
    &lt;h2&gt;Maybe dependencies are a bit of an iceberg&lt;/h2&gt;
    &lt;div class=&quot;grid-2&quot;&gt;
      &lt;div class=&quot;copy&quot;&gt;
        &lt;p&gt;When we look at a &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;build.gradle&lt;/code&gt;, we only see part of the system&#39;s technical surface. Underneath it sit transitive libraries, version couplings, autoconfiguration, proxies, annotation processing, and conventions that someone has to understand the day something breaks.&lt;/p&gt;
        &lt;p&gt;Counting JAR files therefore feels like a blunt measure. A small external dependency can be very cheap. Four hundred lines of hand-rolled OAuth code can be very expensive.&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;So by minimalism I don&#39;t mean the least code, or zero libraries.&lt;/strong&gt; Rather, less unnecessary semantics – less technology the team has to carry over time.&lt;/p&gt;
      &lt;/div&gt;
      &lt;figure class=&quot;figure&quot;&gt;&lt;img src=&quot;https://zero-duplications.com/assets/images/figure-02.webp&quot; alt=&quot;Iceberg where a small visible tip represents direct dependencies and a much larger structure below the surface represents the hidden maintenance surface.&quot; /&gt;&lt;figcaption&gt;Direct dependencies are the visible part. My sense is that maintenance cost usually sits in everything that comes along with them.&lt;/figcaption&gt;&lt;/figure&gt;
    &lt;/div&gt;
    &lt;div class=&quot;metrics&quot;&gt;
      &lt;div class=&quot;metric&quot;&gt;&lt;strong&gt;Versions&lt;/strong&gt;&lt;span&gt;Upgrades, compatibility, and coordination.&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;metric&quot;&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;span&gt;CVEs, parser and network surface, patch work.&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;metric&quot;&gt;&lt;strong&gt;Semantics&lt;/strong&gt;&lt;span&gt;Hidden rules, proxy behavior, and lifecycle.&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;metric&quot;&gt;&lt;strong&gt;Change&lt;/strong&gt;&lt;span&gt;How much needs to be understood next time requirements shift?&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section callout big&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;03 · A proposed order&lt;/p&gt;
    &lt;h2&gt;What if we asked in this order?&lt;/h2&gt;
    &lt;p class=&quot;copy&quot;&gt;Instead of starting with &quot;which library should replace this?&quot;, you could work through four steps. The further right we get, the more specialized semantics we choose to let someone else own. The order isn&#39;t a truth – mostly a way to make the choice deliberate.&lt;/p&gt;
    &lt;div class=&quot;flow&quot;&gt;
      &lt;div class=&quot;step&quot;&gt;&lt;b&gt;1. Is it needed at all?&lt;/b&gt;&lt;span&gt;Is the behavior needed in the first place? A cache or internal event bus that never gets built has no implementation to maintain.&lt;/span&gt;&lt;span class=&quot;arrow&quot;&gt;→&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;step&quot;&gt;&lt;b&gt;2. Is it in the JDK?&lt;/b&gt;&lt;span&gt;Is there a direct, sufficient solution in Java 25? For example an HTTP client, files, Base64, scheduling.&lt;/span&gt;&lt;span class=&quot;arrow&quot;&gt;→&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;step&quot;&gt;&lt;b&gt;3. Does a bit of our own code suffice?&lt;/b&gt;&lt;span&gt;Is the semantics simple and local? Mapping, configuration, a small retry loop, or a composition root can turn out to be fairly clear, ordinary Java.&lt;/span&gt;&lt;span class=&quot;arrow&quot;&gt;→&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;step&quot;&gt;&lt;b&gt;4. Let the specialist handle it&lt;/b&gt;&lt;span&gt;Is it about JSON, OAuth/OIDC, connection pooling, broker protocols, or advanced telemetry? Then I think a mature library is almost always cheaper.&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;04 · Where Spring might fit best&lt;/p&gt;
    &lt;h2&gt;What if Spring lived at the edge?&lt;/h2&gt;
    &lt;p class=&quot;copy&quot;&gt;The idea I personally find most interesting isn&#39;t about removing Spring Boot. It&#39;s about maybe no longer letting Spring be the application&#39;s primary programming model.&lt;/p&gt;
    &lt;div class=&quot;arch&quot; role=&quot;img&quot; aria-label=&quot;Architecture diagram: the outside world reaches Spring at the edge, then a core in plain Java, then adapters to external systems.&quot;&gt;
      &lt;div class=&quot;arch-grid&quot;&gt;
        &lt;div class=&quot;arch-node&quot;&gt;&lt;div class=&quot;arch-label&quot;&gt;Outside world&lt;/div&gt;&lt;div class=&quot;arch-title&quot;&gt;HTTP · identity · operations&lt;/div&gt;&lt;div class=&quot;arch-copy&quot;&gt;Protocols, routing, security, and standardized observability are complex and change over time.&lt;/div&gt;&lt;/div&gt;
        &lt;div class=&quot;arch-node edge&quot;&gt;&lt;div class=&quot;arch-label&quot;&gt;The edge&lt;/div&gt;&lt;div class=&quot;arch-title&quot;&gt;Spring where it helps&lt;/div&gt;&lt;div class=&quot;arch-copy&quot;&gt;WebMVC, Security, adapters, and bootstrap. The framework translates into the application&#39;s own types.&lt;/div&gt;&lt;/div&gt;
        &lt;div class=&quot;arch-node core&quot;&gt;&lt;div class=&quot;arch-label&quot;&gt;The core&lt;/div&gt;&lt;div class=&quot;arch-title&quot;&gt;Plain Java&lt;/div&gt;&lt;div class=&quot;arch-copy&quot;&gt;Domain, use cases, records, constructors, invariants, and small ports without Spring types.&lt;/div&gt;&lt;/div&gt;
        &lt;div class=&quot;arch-node ext&quot;&gt;&lt;div class=&quot;arch-label&quot;&gt;Boundaries&lt;/div&gt;&lt;div class=&quot;arch-title&quot;&gt;Small adapters&lt;/div&gt;&lt;div class=&quot;arch-copy&quot;&gt;JDBC, JSON, external APIs, and queues are wrapped behind clear interfaces.&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;p class=&quot;copy&quot; style=&quot;margin-top:24px&quot;&gt;My thinking is that if the core is free of Spring, the next decision becomes less dramatic. A complex service can happily stay on trimmed-down Boot. A small internal service could later move further toward plain JDK. A batch job or worker might never have needed Spring in the first place. But that&#39;s just a thought – I don&#39;t know how well it holds up in your systems.&lt;/p&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;05 · A few things I&#39;m curious about&lt;/p&gt;
    &lt;h2&gt;Not everything probably needs the same treatment&lt;/h2&gt;
    &lt;div class=&quot;cards&quot;&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag remove&quot;&gt;Maybe simpler today&lt;/span&gt;&lt;h3&gt;Lombok and mappers&lt;/h3&gt;&lt;p&gt;Records and explicit code mean some of the mechanical help feels less valuable than it used to.&lt;/p&gt;&lt;/div&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag remove&quot;&gt;JDK is often enough&lt;/span&gt;&lt;h3&gt;Outbound HTTP&lt;/h3&gt;&lt;p&gt;The JDK HttpClient seems to go a long way for ordinary service-to-service communication.&lt;/p&gt;&lt;/div&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag assess&quot;&gt;Depends on the service&lt;/span&gt;&lt;h3&gt;Spring DI&lt;/h3&gt;&lt;p&gt;Constructor injection needs no container. At the same time, a large and dynamic object graph can very well justify Spring.&lt;/p&gt;&lt;/div&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag assess&quot;&gt;Worth a look&lt;/span&gt;&lt;h3&gt;JPA / Hibernate&lt;/h3&gt;&lt;p&gt;For clear SQL and a limited model, JDBC can be simpler. For rich object graphs, an ORM is probably still cheaper.&lt;/p&gt;&lt;/div&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag keep&quot;&gt;I&#39;d keep this here&lt;/span&gt;&lt;h3&gt;JSON and security&lt;/h3&gt;&lt;p&gt;General-purpose JSON parsing and OAuth/OIDC carry difficult semantics that should rarely become in-house infrastructure.&lt;/p&gt;&lt;/div&gt;
      &lt;div class=&quot;card&quot;&gt;&lt;span class=&quot;tag keep&quot;&gt;I&#39;d keep this here&lt;/span&gt;&lt;h3&gt;Connection pool&lt;/h3&gt;&lt;p&gt;JDBC comes with the JDK, but a production-grade pool doesn&#39;t. HikariCP feels like an obvious dependency to keep.&lt;/p&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;06 · What would we gain?&lt;/p&gt;
    &lt;h2&gt;If it holds, it&#39;s about maintenance cost&lt;/h2&gt;
    &lt;p class=&quot;copy&quot;&gt;What would make this idea worth anything isn&#39;t a tidier dependency tree. It&#39;s whether, over several years, the team ends up with less technology to coordinate, fewer layers to understand, and less impact when the platform is upgraded. And that&#39;s of course an assumption that needs to be tested.&lt;/p&gt;
    &lt;div class=&quot;table-wrap&quot;&gt;&lt;table&gt;
      &lt;thead&gt;&lt;tr&gt;&lt;th&gt;If we reduce…&lt;/th&gt;&lt;th&gt;We might gain…&lt;/th&gt;&lt;th&gt;But only if…&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;External dependency families&lt;/td&gt;&lt;td&gt;Fewer separate version and upgrade tracks&lt;/td&gt;&lt;td&gt;We don&#39;t replace them with large in-house frameworks&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Spring types in the domain&lt;/td&gt;&lt;td&gt;Less impact from framework upgrades and simpler tests&lt;/td&gt;&lt;td&gt;The boundaries between core and adapters stay clear&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Runtime magic&lt;/td&gt;&lt;td&gt;More visible control flow and easier debugging&lt;/td&gt;&lt;td&gt;The explicit code stays small and understandable&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;Excessive standardization in code&lt;/td&gt;&lt;td&gt;Less local infrastructure&lt;/td&gt;&lt;td&gt;The organization still standardizes important contracts and operational behavior&lt;/td&gt;&lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;&lt;/div&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section callout big&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;07 · One thing that may have changed&lt;/p&gt;
    &lt;h2&gt;More code can sometimes mean a smaller system&lt;/h2&gt;
    &lt;p&gt;AI makes it cheaper to produce mechanical mappers, JDBC row mappers, config records, adapters, tests, and repetitive decorators. If that&#39;s true, it lowers the value of abstractions whose main contribution is that we get to skip writing a few lines.&lt;/p&gt;
    &lt;p&gt;At the same time, AI hardly makes protocols, cryptography, parser edge cases, concurrency, or transaction semantics any less dangerous. It&#39;s quick to generate a connection pool. That doesn&#39;t mean the team should own one.&lt;/p&gt;
    &lt;div class=&quot;rule&quot;&gt;A bit more &lt;em&gt;visible code&lt;/em&gt; could add up to a considerably smaller system.&lt;/div&gt;
  &lt;/section&gt;
  &lt;section class=&quot;section&quot;&gt;
    &lt;p class=&quot;eyebrow&quot;&gt;08 · Where I&#39;ve landed so far&lt;/p&gt;
    &lt;h2&gt;Maybe minimalism is mostly about choosing responsibility&lt;/h2&gt;
    &lt;p class=&quot;copy&quot;&gt;I don&#39;t see the point in making &quot;zero Spring&quot; a creed. But I do think there&#39;s something to making every dependency a deliberate choice.&lt;/p&gt;
    &lt;p class=&quot;copy&quot;&gt;A small service might get by on the JDK&#39;s HTTP server. A complex public-facing service can happily stay on WebMVC and Spring Security. A worker might end up being nearly plain Java. What they&#39;d have in common is that the business logic gets to be ordinary Java, and specialist libraries are kept at clear boundaries.&lt;/p&gt;
    &lt;div class=&quot;quote&quot;&gt;Ask first whether it&#39;s needed. Then check the JDK. Write small, concrete code when the semantics are your own. And let specialist libraries take on what belongs to a hard protocol, a security standard, or a resource manager.&lt;/div&gt;
    &lt;div class=&quot;note&quot;&gt;&lt;strong&gt;This is an idea I&#39;d like to keep thinking through together with others – not a migration recipe.&lt;/strong&gt; I&#39;m fully prepared for it to hold up worse in reality than on paper. The question I find interesting isn&#39;t &quot;can we remove Spring?&quot;, but &quot;which technology actually gives the lowest total cost of ownership for this particular service?&quot; If you&#39;ve seen evidence pointing the other way, I&#39;d genuinely like to hear it.&lt;/div&gt;
  &lt;/section&gt;
</content>
  </entry>
</feed>
