<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Axelix Labs</title>
    <description>The latest articles on DEV Community by Axelix Labs (axelix-labs).</description>
    <link>https://dev.to/axelix-labs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F13959%2Fdcb928d4-7e87-4833-96bf-7def9f88f907.png</url>
      <title>DEV Community: Axelix Labs</title>
      <link>https://dev.to/axelix-labs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/axelix-labs"/>
    <language>en</language>
    <item>
      <title>Axelix goes GA. A journey of a thousand miles begins with a single step</title>
      <dc:creator>Sergei Cherkasov</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:39:57 +0000</pubDate>
      <link>https://dev.to/axelix-labs/axelix-goes-ga-a-journey-of-a-thousand-miles-begins-with-a-single-step-5hfj</link>
      <guid>https://dev.to/axelix-labs/axelix-goes-ga-a-journey-of-a-thousand-miles-begins-with-a-single-step-5hfj</guid>
      <description>&lt;p&gt;On behalf of the core Axelix team, and everybody who has contributed to the community, I want to declare: we finally did it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Axelix, finally, &lt;a href="https://github.com/axelixlabs/axelix/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;goes GA (Generally Available)!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For those who do not know - Axelix is a product with an Open Source core, that allows you to discover the common problems, pitfalls and inefficiencies in Java applications at large scale.&lt;/p&gt;

&lt;p&gt;We're available &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; (btw - give us a star!).&lt;/p&gt;

&lt;p&gt;In this post, I want to share the story and the motivation behind the product overall. I hope you find it interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story. Big "Why" Behind Axelix
&lt;/h2&gt;

&lt;p&gt;Java is quite an interesting language and ecosystem in general. I think a lot of people will not argue that it is quite old, and it was one of the first so-called "Object-Oriented" languages, that actually gained massive adoption. It both was, and it still is the backbone of modern enterprise.&lt;/p&gt;

&lt;p&gt;For anyone who claims that Java is dead - I recommend checking the &lt;a href="https://devecosystem-2025.jetbrains.com/" rel="noopener noreferrer"&gt;JetBrains State of Developer Ecosystem survey&lt;/a&gt; or even the &lt;a href="https://survey.stackoverflow.co/2025/technology" rel="noopener noreferrer"&gt;Stack Overflow survey for 2025&lt;/a&gt; (and Stack Overflow has, sadly, become a part of history). It is clear that Java as a language and the "ecosystem" around it (including Kotlin) is still relatively popular, and it remains true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ecosystems around Languages
&lt;/h2&gt;

&lt;p&gt;The experienced developer knows that today's ecosystems that evolve around languages are typically very diverse. For example, let's talk about JavaScript. If we decide to run JavaScript on the server, then we're probably going to work with a database of some sort. Therefore, we're also going to need a framework, a library to work with the database, e.g. an ORM (I know that we may work without it but let's leave that aside).&lt;/p&gt;

&lt;p&gt;And in JavaScript, we have quite a lot of options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prisma&lt;/li&gt;
&lt;li&gt;TypeORM&lt;/li&gt;
&lt;li&gt;DrizzleORM&lt;/li&gt;
&lt;li&gt;Kysely and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can pretty safely state that &lt;a href="https://www.prisma.io/blog/how-prisma-orm-became-the-most-downloaded-orm-for-node-js" rel="noopener noreferrer"&gt;Prisma ORM is &lt;em&gt;probably&lt;/em&gt; the most used ORM on JavaScript&lt;/a&gt;. But notice that it is far from being the definitive JavaScript ORM.&lt;/p&gt;

&lt;p&gt;It is not like Prisma is the default choice and is by far the most popular ORM - it is just not the case. I want to emphasize - even by Prisma's own assessment of its popularity, it is not far ahead of its competitors. They are all very close. &lt;strong&gt;So, the way people work with the database in JavaScript may differ heavily from team-to-team.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The defining trait of Java Ecosystem
&lt;/h2&gt;

&lt;p&gt;In Java, if we're going to take the same problem (working with the database), then we will quickly realize, that the industry is heavily concentrated.&lt;/p&gt;

&lt;p&gt;We generally can state that Spring Data JPA as a solution around Hibernate occupies above 50% of all the data access solutions, let alone ORMs. I remember some time ago Josh Long, the much-loved Developer Advocate for Spring, posted a survey on X (formerly Twitter) about the data access technologies usage:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1528655715933597697-592" src="https://platform.twitter.com/embed/Tweet.html?id=1528655715933597697"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1528655715933597697-592');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1528655715933597697&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;We must of course acknowledge that there are fast-growing alternative solutions to JPA that are explored, such as jOOQ, but Spring Data JPA dominates the field. So, the way we work with the database in Java (at least on the server side) is relatively well-defined and revolves around Spring Data JPA.&lt;/p&gt;

&lt;p&gt;But there is more to it, it is not just the ORMs or data-access libraries. It's something bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring Framework as the Phenomenon
&lt;/h2&gt;

&lt;p&gt;We cannot talk about Java ecosystem and not talk about Spring Framework. If we're going to look in the &lt;a href="https://devecosystem-2025.jetbrains.com/" rel="noopener noreferrer"&gt;aforementioned Developer Ecosystem State report from JetBrains&lt;/a&gt;, we're going to get the following insights. I have assembled them carefully for you - share them freely as you want. Also note that the respondents had multiple choices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Share of framework users&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3,042&lt;/td&gt;
&lt;td&gt;87.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ktor&lt;/td&gt;
&lt;td&gt;362&lt;/td&gt;
&lt;td&gt;10.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarkus&lt;/td&gt;
&lt;td&gt;287&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;210&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vaadin&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Micronaut&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grails&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;1.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Helidon&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is (at least AFAIK) one of the most recent and most trusted data we have. Many thanks to JetBrains for publishing it.&lt;/p&gt;

&lt;p&gt;So, yes, although other ecosystems are also growing, e.g. Quarkus along with its Quarkiverse, but they still have a very, &lt;em&gt;very&lt;/em&gt; long way to catch up with Spring Framework. So at the end of 2025 (I wager that even today) we can safely state:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The most common Java app, which &lt;strong&gt;by far&lt;/strong&gt; represents more than half of Java server side deployments is a Spring Framework app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, it is just data, you can download it and check yourself.&lt;/p&gt;

&lt;p&gt;But even if we consider different ecosystems: Spring Framework, Quarkus or Micronaut etc - we're going to quickly realize that these are not just "libraries" or "frameworks" in a traditional sense. &lt;strong&gt;They are themselves large frameworks that build an entire sub-ecosystem around them&lt;/strong&gt;. For example, are you working with Data? Well, you have&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Data in Spring&lt;/li&gt;
&lt;li&gt;Jakarta Data in Quarkus&lt;/li&gt;
&lt;li&gt;Micronaut Data in Micronaut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oh, you want to send an HTTP request? The most common thing on the backend! Well, we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Cloud Exchange/OpenFeign in Spring&lt;/li&gt;
&lt;li&gt;RestClients in Quarkus&lt;/li&gt;
&lt;li&gt;Declarative HTTP Clients in Micronaut&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that every framework has its own solution.&lt;/p&gt;

&lt;p&gt;In other ecosystems, e.g. in JavaScript on the server side, we may use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ExpressJS for web server&lt;/li&gt;
&lt;li&gt;Axios for sending requests&lt;/li&gt;
&lt;li&gt;Prisma as an ORM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And these are all just individual choices made by the team. We may use Axios or we may use Got. We may use Prisma or we may use TypeORM. Some solutions are more popular than others, but they are all independent - they exist outside of a single umbrella, like Spring Framework, for example.&lt;/p&gt;

&lt;p&gt;To put it simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the Java world, if we have any kind of problem, most likely, we &lt;strong&gt;do NOT&lt;/strong&gt; need to seek the solution on GitHub, find some fork that does what you want (which is a common JavaScript practice btw). There is almost 100% chance that Spring Framework has a solution for us already.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why am I telling you about this?
&lt;/h2&gt;

&lt;p&gt;Because it is important to understand, that since Java Ecosystem is so solidified around the Spring Framework technological stack, &lt;strong&gt;it often suffers the same problems in production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What are these problems? Well, there are a lot of them. And we know all of them with our very own butts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our connection pool leaks&lt;/li&gt;
&lt;li&gt;Our API endpoints are performing slow (maybe the problem is in the database?)&lt;/li&gt;
&lt;li&gt;We accidentally expose &lt;code&gt;/actuator/env&lt;/code&gt; in production and leak secrets&lt;/li&gt;
&lt;li&gt;Our applications consume too much memory and start very slowly...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so on. And the industry, throughout decades of Java experience in production, has developed a set of recommendations, best-practices, "solutions", guidelines. And these guidelines exist on different levels of the application, e.g:&lt;/p&gt;

&lt;p&gt;Guidelines for Hibernate/ORM&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/entity-mappings-introduction-jpa-fetchtypes/" rel="noopener noreferrer"&gt;Do not use &lt;code&gt;EAGER&lt;/code&gt; fetch, ever!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/" rel="noopener noreferrer"&gt;Do not use &lt;code&gt;CascadeType.ALL&lt;/code&gt; or &lt;code&gt;CascadeType.REMOVE&lt;/code&gt; for "XToMany"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/best-practices-for-many-to-many-associations-with-hibernate-and-jpa/" rel="noopener noreferrer"&gt;Do not use &lt;code&gt;List&lt;/code&gt; for &lt;code&gt;@ManyToMany&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/hibernate-tips-unidirectional-one-to-many-association-without-junction-table/" rel="noopener noreferrer"&gt;Do not use one-directional &lt;code&gt;@OneToMany&lt;/code&gt;!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and so on. Guidelines for Spring performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/java/comments/1hcgyc4/the_opensessioninview_pattern_of_spring_boot_a/" rel="noopener noreferrer"&gt;Disable OSIV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://rieckpil.de/achieve-faster-build-times-with-the-spring-test-profiler/" rel="noopener noreferrer"&gt;Spring Test Profiler!&lt;/a&gt; (many thanks to Philip Riecks for such a tool)&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://github.com/seregamorph/spring-test-smart-context" rel="noopener noreferrer"&gt;Spring smart context caching!&lt;/a&gt; (Sergey Chernov, a highly experienced Engineer and a buddy of mine, developed the tool)!&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vladmihalcea.com/log-sql-spring-boot/" rel="noopener noreferrer"&gt;Do not set &lt;code&gt;spring.jpa.show-sql=true&lt;/code&gt; or other dangerous properties in production!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and so on. Guidelines for even our beloved Java!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For Christ’s sake, &lt;a href="https://openjdk.org/projects/leyden/" rel="noopener noreferrer"&gt;use Project Leyden&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;Use Compact Object Headers (&lt;a href="https://openjdk.org/projects/lilliput/" rel="noopener noreferrer"&gt;Project Lilliput&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Configure the &lt;a href="https://blog.gceasy.io/java-garbage-collection-best-practices/" rel="noopener noreferrer"&gt;logging of GC&lt;/a&gt; in production!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so on and on and on. Because we have that solidified ecosystem we generally know &lt;em&gt;"what is right and what is wrong"&lt;/em&gt;. And for the long time there was no such tool.&lt;/p&gt;

&lt;p&gt;But we thought that this is wrong. And we probably should have one. And this tool is called &lt;strong&gt;Axelix&lt;/strong&gt; (&lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;The source code&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In 1.0.0, we do not support the detection of all the issues that I outlined above (most, but not all). And the above is just an example. There is a lot (really, &lt;em&gt;a lot&lt;/em&gt;) of knowledge that we as a Java community have accumulated throughout these years that we're going to put inside Axelix, so it can serve you well.&lt;/p&gt;

&lt;p&gt;I hope, at that point, the motivation behind the tool is clear, and now, I want to answer a couple of questions that we already have received many times over during Milestones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: Is it free?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes, it is&lt;/strong&gt;. We're going to follow the Open Core model. This is the Open Source software development model that underpins projects that we all love and care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;li&gt;Keycloak&lt;/li&gt;
&lt;li&gt;Vaadin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And so on. It is &lt;a href="https://opencoreventures.com/insights/someone-has-to-pay-for-the-domino/" rel="noopener noreferrer"&gt;generally considered to be the most reliable model for the OSS development&lt;/a&gt;, and that is why industry leaders choose, and that is why we choose it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: Is the project ready for use and deploy?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes&lt;/strong&gt;, it is ready for use free of charge. We have processed the feedback from literally tens of teams that have installed Axelix milestone releases and shared their insights with us.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: Is it safe to deploy in production?
&lt;/h3&gt;

&lt;p&gt;It is designed for production, so the answer is yes. But since this is the first 1.0.0 GA release, the general recommendation is to first install it on development environments, and then for production.&lt;/p&gt;

&lt;p&gt;That said, we do have teams that deployed Axelix in production already to get insights about their &lt;code&gt;@Transactional&lt;/code&gt; methods behavior and properties resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottom Line:&lt;/strong&gt; The core stays Open Source always, and it always will be free of charge, and it already provides value for the teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Are We Headed?
&lt;/h2&gt;

&lt;p&gt;The team behind Axelix has many years of experience writing and optimizing Spring Boot applications. We have a lot of work to do ahead of us, and we still have a lot of things to incorporate into Axelix.&lt;/p&gt;

&lt;p&gt;For those who want to know how the project is installed &amp;amp; configured, please, visit our &lt;a href="https://axelix.io" rel="noopener noreferrer"&gt;website&lt;/a&gt; and also our &lt;a href="https://axelix.io/docs" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. In case of any questions or difficulties, you can always reach out to the team at: &lt;a href="mailto:hello@axelix.io"&gt;hello@axelix.io&lt;/a&gt; We, the maintainers, are happy to answer your questions and assist you during the installation.&lt;/p&gt;

&lt;p&gt;We're also going to stay committed to Open Source, and committed to our values, which are making sure that your Java applications are as secure, performant and efficient as they can be.&lt;/p&gt;

&lt;p&gt;Thank you all. Mikhail.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>spring</category>
    </item>
    <item>
      <title>Taming Hibernate in Tests</title>
      <dc:creator>Sergei Cherkasov</dc:creator>
      <pubDate>Sat, 08 Aug 2026 07:51:22 +0000</pubDate>
      <link>https://dev.to/axelix-labs/taming-hibernate-in-tests-hl6</link>
      <guid>https://dev.to/axelix-labs/taming-hibernate-in-tests-hl6</guid>
      <description>&lt;p&gt;Hi everyone! This is Mikhail Polivakha, the technical lead of the &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;Open Source Axelix project&lt;/a&gt; (a project dedicated to helping you identify common problems in Java server-side applications, including while working with Hibernate).&lt;/p&gt;

&lt;p&gt;In this article, I want to address the question that I think is pretty common amongst the advanced Hibernate users:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is there a verify Hibernate behavior in tests (like Junit for example)?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here it's really important not to hand you a "fish", but to teach you how to fish - that is, to give you a framework of what's worth keeping in mind when working with Hibernate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling Hibernate during tests. Overview.
&lt;/h2&gt;

&lt;p&gt;If I asked you: &lt;strong&gt;guys, which problems in Hibernate annoy you the most?&lt;/strong&gt; You'd mention things like the sneaky N + 1, In Memory Pagination (when explicitly specifying &lt;code&gt;setMaxResult&lt;/code&gt; or a &lt;code&gt;Pageable&lt;/code&gt;), a Cartesian Product loaded into the application's memory, or, say, some baffling swarm of &lt;code&gt;SELECT&lt;/code&gt;s followed by &lt;code&gt;UPDATE&lt;/code&gt;s/&lt;code&gt;DELETE&lt;/code&gt;s under the hood, and so on.&lt;/p&gt;

&lt;p&gt;Everything I listed above, and really all "problems" with Hibernate, can be split into 2 categories. Let's break them down.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Problems at the ORM level
&lt;/h2&gt;

&lt;p&gt;There are situations that both we (the end users) and Hibernate identify as problems. For example, up until Hibernate 7.4, the query below would perform pagination entirely in memory. This is fairly well known:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT o FROM Owner o JOIN FETCH o.pets"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;countQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT COUNT(o) FROM Owner o"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllWithPets&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In version 7.4 Hibernate learned to do pagination within a sub-select. But nevertheless, if you, for example, work on Spring Boot 3, you have Hibernate 6, and in the case of queries like the one above there will be a warning in stdout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it's there because Hibernate itself understands that its actions &lt;em&gt;may&lt;/em&gt; blow up your heap. There's even a property that we at Axelix recommend setting for new microservices that don't use 7.4 yet (and there are still many of those):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you set it, Hibernate will simply throw an exception where it would previously do In Memory Pagination.&lt;/p&gt;

&lt;p&gt;In short, what I want to say is that the problem above is understood by Hibernate as a real problem. It recognizes and admits it. That's one layer of problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But oddly enough, these are quite hard to track&lt;/strong&gt;. Hibernate rarely provides any means to hook into it (although there are some nuances, read below). For instance, at Axelix, to detect In Memory Pagination (if it happens), we simply &lt;a href="https://github.com/axelixlabs/axelix/blob/master/sbs/axelix-spring-boot-2-starter/src/main/java/com/axelixlabs/axelix/sbs/spring/core/persistence/hibernate/LogbackInMemoryPaginationAppender.java" rel="noopener noreferrer"&gt;literally catch an &lt;code&gt;ILoggingEvent&lt;/code&gt; and inspect the code within it&lt;/a&gt;. Because there are no other acceptable ways (at least none that we're aware of)!&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Second. Problems at the application level
&lt;/h2&gt;

&lt;p&gt;There's a conceptually different layer of problems. N + 1, for example, belongs to it.&lt;/p&gt;

&lt;p&gt;Let's think for a second: &lt;em&gt;what is N + 1?&lt;/em&gt;. Try right now, without looking anywhere, to clearly formulate the definition in your head. Done? Here's how I'd formulate it (informally):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's a situation in which you iterate over a collection loaded by Hibernate, sequentially accessing some lazy field within each individual entity of the collection, thereby triggering "N" additional SELECT queries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is, if you think about it a little, it becomes obvious that for Hibernate this is just Lazy Loading. The notion of N + 1 as a problem exists at the application level, not at the ORM level. &lt;strong&gt;That is, the problem is not lazy loading itself, but the semantics of lazy loading&lt;/strong&gt; (what does &lt;em&gt;this particular&lt;/em&gt; lazy loading mean? Is it just lazy loading, or lazy loading within an N + 1?).&lt;/p&gt;

&lt;p&gt;In such a situation it's obviously pointless to rely on Hibernate, since the N + 1 phenomenon happens at the application level, and the ORM is completely unaware of it — it's not its area of responsibility. You can't get by without fairly strong tooling around Hibernate. &lt;strong&gt;Can it be written, and if so, how?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If we're talking specifically about N + 1, then the answer is generally - yes, you can, and we did it at Axelix in Open Source. Let me tell you how exactly we did it. &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;The source code is open, take a look if you're interested&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The General Approach to N + 1 Detection
&lt;/h3&gt;

&lt;p&gt;Hibernate doesn't know what N + 1 is, but it does know what a Lazy Loading association is, and that moment can already be caught. For example, here's how we do it in &lt;a href="https://github.com/axelixlabs/axelix/blob/master/sbs/axelix-spring-boot-2-starter/src/main/java/com/axelixlabs/axelix/sbs/spring/core/persistence/hibernate/NPlusOneCollectionLoadListener.java" rel="noopener noreferrer"&gt;&lt;code&gt;NPlusOneCollectionLoadListener&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onInitializeCollection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InitializeCollectionEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;PersistentCollection&lt;/span&gt; &lt;span class="n"&gt;persistentCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCollection&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;EventSource&lt;/span&gt; &lt;span class="n"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;CollectionPersister&lt;/span&gt; &lt;span class="n"&gt;collectionPersister&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eventSource&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPersistenceContextInternal&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCollectionEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persistentCollection&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLoadedPersister&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="nc"&gt;LazyLoadingTarget&lt;/span&gt; &lt;span class="n"&gt;lazyLoadingTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseLazyLoadingTarget&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collectionPersister&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRole&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lazyLoadingTarget&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;transactionAccessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordLazyLoading&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lazyLoadingTarget&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The general "role" format is expected to look like this: com.example.Order.items&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nd"&gt;@Nullable&lt;/span&gt; &lt;span class="nc"&gt;LazyLoadingTarget&lt;/span&gt; &lt;span class="nf"&gt;parseLazyLoadingTarget&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;separatorIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lastIndexOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ownerEntityClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separatorIndex&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separatorIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LazyLoadingTarget&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerEntityClass&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClassNotFoundException&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;IndexOutOfBoundsException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Unexpected propertyPath format '{}'. Axelix cannot recognize that, so lazy loading and potential N + 1 is not going to be tracked for this property"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// it means that the role format is not the one that we expect&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, through a series of manipulations, we can figure out which association was loaded lazily and on which collection. What do we then do with this information?&lt;/p&gt;

&lt;p&gt;Axelix, for example, currently makes the following decision: &lt;strong&gt;If we noticed that within a transaction there were several lazy loadings of the same association, then we consider this to be an N + 1.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is that always correct? Well, this is debatable, because, for instance, in this code example, if we assume that &lt;code&gt;Order.items&lt;/code&gt; were loaded lazily — we, &lt;em&gt;seemingly&lt;/em&gt;, don't have an N + 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;compareOrders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;previousId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currentId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previousId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Two lazy accesses to Order.items, but there's no collection of orders here -&lt;/span&gt;
    &lt;span class="c1"&gt;// we're just comparing two specific versions of the same order&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The order contents have changed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or do we? And what if I change this example to something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;compareOrders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;previousId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;currentId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAllById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previousId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Exactly the same comparison as above, but now within an iteration&lt;/span&gt;
    &lt;span class="c1"&gt;// over a loaded collection of orders: at each step there's again a lazy&lt;/span&gt;
    &lt;span class="c1"&gt;// access to Order.items on both previous and current&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The order contents have changed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does it look like N + 1 now? It does.&lt;/p&gt;

&lt;p&gt;So, in short, what I want to say is that here you need to clearly fix the definition of N + 1 at the application level. We at Axelix deliberately decided that this case is nonetheless worth treating as an N + 1, so, in the UI, it will be reported to you:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frp5l1stk8eqmyd15dyhv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frp5l1stk8eqmyd15dyhv.png" alt="Axelix reporting an N + 1 in the UI" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S: Among other things, there's a small nuance here: N + 1, strictly speaking, can also happen &lt;strong&gt;without an open transaction&lt;/strong&gt; when OSIV is enabled. That's a separate case which we're not considering for now, since it would complicate the picture.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro-Conclusion
&lt;/h3&gt;

&lt;p&gt;I'd like to draw a small micro-conclusion here. In general, detecting some complex phenomena that occur at the application level while working with Hibernate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N + 1&lt;/li&gt;
&lt;li&gt;Blocking calls inside transactions, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be done. It just requires you to write a fairly large and non-trivial amount of tooling. Using N + 1 as an example, I hope you've roughly understood what such things will look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  General Hibernate Problems
&lt;/h2&gt;

&lt;p&gt;As a separate section, I'd like to address general Hibernate problems. Quite often people have some conditional hot-path for saving or updating data. And devs simply want to make sure that&lt;br&gt;
&lt;strong&gt;Hibernate doesn't "sabotage" the entire process undercover&lt;/strong&gt;. Sounds familiar, right?&lt;/p&gt;

&lt;p&gt;Here you need to take a step back and ask: &lt;strong&gt;and what do we mean by "sabotage"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Having talked with people, we'll discover that most often people want roughly the following: to have confidence that a person wrote &lt;code&gt;repository.save()&lt;/code&gt;, and that it will execute specifically an &lt;code&gt;INSERT&lt;/code&gt; into the table and nothing more.&lt;/p&gt;

&lt;p&gt;For example, having this piece of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OwnerService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OwnerRepository&lt;/span&gt; &lt;span class="n"&gt;ownerRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OwnerService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OwnerRepository&lt;/span&gt; &lt;span class="n"&gt;ownerRepository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ownerRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ownerRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Owner&lt;/span&gt; &lt;span class="nf"&gt;registerOwner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ownerRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The person wants to be sure that there won't be any hidden &lt;code&gt;SELECT&lt;/code&gt;s and so on there. I think some people recognized themselves. &lt;strong&gt;Can this be done?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;generally yes, it can&lt;/strong&gt;. Hibernate has a Statistics API, which on the one hand isn't part of the JPA standard, and on the other hand can give us some details about query execution within a session. For example, for the code above you can write a test like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Requires hibernate.generate_statistics=true&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OwnerServiceTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OwnerService&lt;/span&gt; &lt;span class="n"&gt;ownerService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;EntityManagerFactory&lt;/span&gt; &lt;span class="n"&gt;entityManagerFactory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;registerOwner_issuesExactlyOneInsert&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// given&lt;/span&gt;
        &lt;span class="nc"&gt;Statistics&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityManagerFactory&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unwrap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatistics&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// when&lt;/span&gt;
        &lt;span class="n"&gt;ownerService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerOwner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"John"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Doe"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// then&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityInsertCount&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;isEqualTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityUpdateCount&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;isZero&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntityDeleteCount&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;isZero&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API lets you verify that only one &lt;code&gt;INSERT&lt;/code&gt; was executed within the method. It works based on the fact that in the test I opened a transaction (Spring Boot Test of course recognizes this and opens a transaction that will have to roll back at the end),&lt;br&gt;
thereby opening a Hibernate &lt;code&gt;Session&lt;/code&gt; - this is the default behavior, so I think there are no surprises here.&lt;/p&gt;

&lt;p&gt;Since the Statistics API collects statistics at the &lt;code&gt;SessionFactory&lt;/code&gt; level, not at the level of individual &lt;code&gt;Session&lt;/code&gt;s, in each test we have to access the &lt;code&gt;SessionFactory&lt;/code&gt; in order to first clear all its statistics.&lt;br&gt;
And only then can we make the checks we're interested in (Strictly speaking, some telemetry is also collected at the &lt;code&gt;Session&lt;/code&gt; level, but it's rather scarce and in practice can help you little).&lt;/p&gt;

&lt;p&gt;Overall, the telemetry collected (the number of &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; statements, and so on) will be enough for you to make some basic checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;In practice, the answer to the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is there a verify Hibernate behavior in tests (like Junit for example)?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Strongly depends on what exactly you want to detect. Some tooling can be written — for example, Axelix can detect both In Memory Pagination, and N + 1, and a number of other problems. In some situations tests will help you a lot.&lt;br&gt;
In general, first of all it's better to ask yourself — &lt;strong&gt;what problem exactly are we trying to solve here&lt;/strong&gt;. And only then build a solution: is it possible (considering what I wrote above) to write an effective test,&lt;br&gt;
is it possible to delegate this to external tooling, or maybe (the ideal case) the problem doesn't need to be solved in the first place.&lt;/p&gt;

&lt;p&gt;Best of luck to everyone!&lt;/p&gt;

</description>
      <category>database</category>
      <category>testing</category>
      <category>springboot</category>
      <category>hibernate</category>
    </item>
    <item>
      <title>Natural IDs in your database. I am telling you the last time!</title>
      <dc:creator>Mikhail Polivakha</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:42:21 +0000</pubDate>
      <link>https://dev.to/axelix-labs/natural-ids-in-your-database-i-am-telling-you-the-last-time-13nc</link>
      <guid>https://dev.to/axelix-labs/natural-ids-in-your-database-i-am-telling-you-the-last-time-13nc</guid>
      <description>&lt;p&gt;Hi everyone! This is Mikhail Polivakha, tech lead of the &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;Axelix project&lt;/a&gt; (btw, give us a star!). In my experience consulting teams that build enterprise applications, I keep getting asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What about natural keys in the database? Say I have a column that lets me explicitly identify a record, should I use it as the Primary Key?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And over my years of designing enterprise systems, and over the time spent designing &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;Axelix&lt;/a&gt;, I've come to a conclusion: &lt;strong&gt;just never use natural keys, ever.&lt;/strong&gt; When you feel the urge to do it, step outside, take a walk, get some fresh air, and it'll pass.&lt;/p&gt;

&lt;p&gt;I understand this answer is categorical, so I'll add a couple of clarifications about what to do if you do happen to have a unique discriminating column that, &lt;em&gt;as it seems to you&lt;/em&gt;, lets you uniquely identify a record in a database table. The full, nuanced answer is of course more complicated, but if I have to give you a straight TL;DR:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When designing new systems, in my opinion, you should &lt;strong&gt;always&lt;/strong&gt; use surrogate primary keys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now let's get into why I think so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules Written in Blood
&lt;/h2&gt;

&lt;p&gt;Rules like the one above are usually born out of getting burned on real projects several times. I have an absolutely perfect story straight from Open Source Axelix. The &lt;a href="https://github.com/axelixlabs/axelix" rel="noopener noreferrer"&gt;source code is on GitHub&lt;/a&gt;, so if you feel like it, go and check for yourself. I won't go too deep into the details, but so that you grasp the depth of the problem, I'll give you a bit of context. In some places I'll also deliberately simplify the parts I consider non-essential to understanding the problem.&lt;/p&gt;

&lt;p&gt;At its core, Axelix consists of two components. The first is &lt;strong&gt;Master&lt;/strong&gt;, a standalone application that acts as the "brain" of the system. It's deployed either in a K8S cluster, or launched as a separate Docker container, or even just run as a plain JAR.&lt;/p&gt;

&lt;p&gt;Master aggregates information from your Spring Boot services and stores it in its database. This information is later used to understand the "maturity" of your ecosystem, the distribution of versions of key components (for example, Spring Boot or Java versions), tracking known tech-debt issues, and so on.&lt;/p&gt;

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

&lt;p&gt;If we picture a typical company, they usually have a K8S/OpenShift cluster where their production runs. Almost always, a given application is deployed to production not as a single copy, but as a set of Instances (a K8S Deployment + a configured HPA, and so on). So in practice we have one logical application that is physically a set of different containers.&lt;/p&gt;

&lt;p&gt;Now I think we have enough context to discuss the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Beginning. Natural Keys: Sure, Why Not!
&lt;/h2&gt;

&lt;p&gt;As I said, Axelix stores data in its database to understand the overall state of your application. Let's call this table "Application" (in reality this abstraction is named differently in Axelix, but again, I'm simplifying heavily). This is where application-level data lives.&lt;/p&gt;

&lt;p&gt;Master can collect data from Spring Boot microservices via both a push and a pull model, but regardless of the model, &lt;strong&gt;it collects data at the Instance level, not at the Application level, i.e. not for the whole application.&lt;/strong&gt; So Master polls each Instance, and it's then Master's job to &lt;strong&gt;somehow figure out&lt;/strong&gt; that all those Instances belong to the same application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foxh6y64qad1lbo56ax0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foxh6y64qad1lbo56ax0e.png" alt="Axelix Instances Discovery" width="800" height="808"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question is: &lt;strong&gt;How is Master supposed to do that?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How does it figure out that these Instances belong to the same application? (Don't forget: Axelix isn't always deployed in K8S. Relying on ClusterIP services and the like is not an option.)&lt;/p&gt;

&lt;p&gt;Actually, if you think about it a little, the solution is right on the surface: &lt;strong&gt;we can just aggregate information at the level of the GroupID/ArtifactID pair from the GAV coordinates (the standard format of a Maven distribution)&lt;/strong&gt;. After all, all the Instances are required to have the same GroupID/ArtifactID, right?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbgqos3pymldw0u2r79b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbgqos3pymldw0u2r79b.png" alt="Axelix Instances Grouping" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Broadly speaking, yes, that's true. Some might think we could key off other things, for example &lt;code&gt;spring.application.name&lt;/code&gt; or similar, but unfortunately that won't work, for a number of reasons. That's another story, though, and it's not important right now.&lt;/p&gt;

&lt;p&gt;So imagine we're designing such a relation in the database. Here's my question for you: &lt;strong&gt;what primary key would you want for a table like this?&lt;/strong&gt; When we designed the "Application" entity, it seemed right to make the &lt;code&gt;{groupId/artifactId}&lt;/code&gt; pair the primary key, i.e. a Natural Composite Key.&lt;/p&gt;

&lt;p&gt;And it's so convenient! When information about some Instance arrives in Axelix Master (whether via the push or the pull model):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can update the data with a simple ANSI SQL &lt;code&gt;MERGE&lt;/code&gt; or &lt;code&gt;INSERT ... ON CONFLICT DO ...&lt;/code&gt;, because artifactId/groupId is the primary key! &lt;a href="https://github.com/spring-projects/spring-data-relational" rel="noopener noreferrer"&gt;Spring Data JDBC (which we use as the ORM in Axelix Master) in 4.1&lt;/a&gt;
finally learned how to do UPSERTs on the primary key, and now we can just do this via &lt;code&gt;JdbcAggregateTemplate&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;reloadCurrentState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BasicRegistrationMetadata&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;jdbcAggregateTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;And how nicely it works out for the front-end! &lt;strong&gt;And here we arrive at the fact that a natural key carries business meaning by itself!&lt;/strong&gt; That, by the way, is one of the genuinely nice properties of natural keys. What do I mean? For example, in a situation where we just need to display the name of our "Application", and the name alone is enough, we can use the &lt;code&gt;artifactId&lt;/code&gt;, i.e. a part of the composite natural key. No need to "fetch anything extra", and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So where's the problem? Given everything I've said so far, is this problem really so critical that I claim you shouldn't use natural keys at all? &lt;strong&gt;Yes, it's that serious. And here's why.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So What's the Deal? A Bit of Philosophy
&lt;/h2&gt;

&lt;p&gt;The older a person gets, the more prone they are to doubting various things (for example, my claim in this article! And that's okay!). This is because people accumulate experience.&lt;/p&gt;

&lt;p&gt;People who have been doing engineering for a good while accumulate experience and come to understand just how much everything changes, and how much they still don't know (experienced engineers understand me 100% right now). A vendor comes and goes. So does an employee. &lt;em&gt;The uniqueness of a natural key...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ray Dalio (an amazing person and macro investor, I highly recommend reading him) &lt;a href="https://www.principles.com/principles/ed86d768-d0b1-4b5c-bada-29592857b274/" rel="noopener noreferrer"&gt;wrote in his book "Principles"&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sincerely believe that you might not know the best possible path and recognize that your ability to deal well with "not knowing" is more important than whatever it is you do know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is incredible wisdom. The idea is to accept the fact that your knowledge of the outside world is limited, and it will always be many times smaller than the set of things you don't know but which nonetheless affect your life/system/etc. And the most important thing in such a situation &lt;strong&gt;is to be able to work WITH YOUR OWN NOT-KNOWING of something, to hedge risks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How does this relate to natural keys?&lt;/p&gt;

&lt;p&gt;Very simply: &lt;strong&gt;if some discriminator seems like an obvious key in the moment, just remember that the scope of your knowledge is incomparably small next to what you don't know. And that "invariant" you're pinning your hopes on, the one you think will be unique: it can very easily stop being unique half a year later.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's more, the scope of your knowledge will keep growing. Over time, &lt;strong&gt;you (yes, you, my friend) grow as an engineer.&lt;/strong&gt; After a while you'll look at this code, or at the design of this system, and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How on earth!? How could I have done this? This crap is just awful, it was obvious this key would break uniqueness in case X!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it'll be obvious to you. &lt;strong&gt;But later.&lt;/strong&gt; When you become wiser. By the way, if you don't have these moments of "enlightenment" in your career, where you scold yourself for your own past decisions, that's a very strong warning sign that you've stopped growing as a specialist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to Engineering
&lt;/h2&gt;

&lt;p&gt;Let's get back a bit closer to the technical side.&lt;/p&gt;

&lt;p&gt;The main point of the previous section is that what seems like the uniqueness of a natural key today can easily stop being uniqueness later. Now let's think like engineers: &lt;strong&gt;how bad is that, really?&lt;/strong&gt; How bad is it that we'll be wrong about our natural key (composite or not, doesn't matter right now) turning out not to be unique?&lt;/p&gt;

&lt;p&gt;The truth is that a record's primary key must &lt;strong&gt;always (!)&lt;/strong&gt; have (among others) the following two distinguishing properties:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It must be immutable
&lt;/h3&gt;

&lt;p&gt;When we assign a record some key by which we identify it, &lt;strong&gt;we then have no right to change it.&lt;/strong&gt; Why? Because the outside world that depends on our system stores exactly this ID, this primary key, to identify the record. It stores a reference, not the record itself.&lt;/p&gt;

&lt;p&gt;For example, imagine you have a third-party service that stores user profiles: &lt;code&gt;user-service&lt;/code&gt;. And there they decided to use email as the natural key. You write a service that orchestrates users' subscriptions to various services within the ecosystem. And now you need to fetch a user's profile from that &lt;code&gt;user-service&lt;/code&gt; system for your operations. How will you fetch it? By email, of course! It's the "unique key", after all.&lt;/p&gt;

&lt;p&gt;And now imagine that the Product Owner comes along and says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our service we want to let a user change the email tied to their account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is, effectively, an already-existing record in the database will have its identity changed. &lt;strong&gt;By changing a record's ID in this &lt;code&gt;user-service&lt;/code&gt;, any other system, including yours, can no longer find the profile it needs.&lt;/strong&gt; That would be a mass incident. That's why an ID must be immutable &lt;strong&gt;always.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It must uniquely identify a record at any moment in time
&lt;/h3&gt;

&lt;p&gt;Now imagine that, all of a sudden, the folks from the team that develops &lt;code&gt;user-service&lt;/code&gt; get a requirement. They're told:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hey, we sometimes run into a situation where a user once created an account and tied their email to it. And now they want to somehow delete the old account (which they created some 10 years ago) and create a new one, and attach the same email to it. We wouldn't want to delete the old account (in enterprise, for various reasons, hard deletes are rarely done). So, shall we do it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here the problem is even more obvious. Not only can your system, which depends on &lt;code&gt;user-service&lt;/code&gt;, no longer find the right user profile (there could be several of them now!), all existing contracts break, and to "fix" them you'll have to "re-define" the ID (it's no longer unique, and you can't rely on the ID alone anymore). And if we have to change the ID, then see the section above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cause of Death: Natural Id
&lt;/h3&gt;

&lt;p&gt;Mistakes come in varying degrees of severity. There are mistakes that have a local effect and can be fixed relatively quickly and easily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But keys that identify data in distributed systems are something that spreads across the entire distributed system into its most varied corners&lt;/strong&gt;. So the moment you suddenly realize with horror that the natural id is no longer unique, the so-called blast radius will be fantastic. Especially in modern microservice architecture.&lt;/p&gt;

&lt;p&gt;So, friends, people die of different causes. Someone died of cancer, someone died of heart failure. And someone simply chose a Natural Id as their primary key, and then received an email in their inbox, or suddenly heard at a daily standup that the uniqueness assumption of this key was about to be shaken.&lt;/p&gt;

&lt;p&gt;I suggest a moment of silence before reading on, in memory of those engineers who paid the price for choosing Natural ID as their primary key…&lt;/p&gt;

&lt;p&gt;Thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Axelix Case
&lt;/h2&gt;

&lt;p&gt;Let's get back to the real case we had at Axelix.&lt;/p&gt;

&lt;p&gt;We haven't hit GA yet (it'll ship this summer, 2026, we're actively working on it), but we already have several Milestone releases. We embed with various companies to gather feedback, potential bugs, problems, and so on.&lt;/p&gt;

&lt;p&gt;And one company tells us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You know, it just so happens that we essentially have two services: service A and service B. They're basically identical, just deployed in different network segments. They have the same groupId and artifactId. Nevertheless, service A is maintained by this team, and service B by that team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've simplified all the details, but this is the general message. So, we have a problem in this case - &lt;strong&gt;we can no longer identify an application the way we wanted, via the artifactId/groupId pair&lt;/strong&gt;. This is exactly what typically&lt;br&gt;
happens after a while, when the system is already deployed in production.&lt;/p&gt;

&lt;p&gt;Remember Ray Dalio!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;... What exists within the area of "not knowing" is so much greater and more exciting than anything any one of us knows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's precisely because of situation like this that you need to ask users to provide Axelix with the information about what the unique ID of a given application is themselves (for example, in &lt;code&gt;application.yaml&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  But Natural IDs Do Have Advantages...
&lt;/h2&gt;

&lt;p&gt;In my experience, the fact that a Natural ID carries business meaning that can be used somewhere (for example, displaying an application's name on the UI as the artifactId, as I already showed with the Axelix example) - &lt;strong&gt;this is solved simply by designing your API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, even with surrogate keys, you can design your API so that you don't have to fetch extra data from the backend; it's not a big problem (for example, get some metadata, put it into the state manager on the front-end, and so on; there are plenty of ways).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's really important about natural keys is that they force you to think about the invariants of your data&lt;/strong&gt;. That is, for example, logically, if your email is unique, then it makes sense to create an index on it (which is, for instance, what Postgres does when you ask it to create a Primary Key). And to avoid having two different indexes, why not make email the primary key, since in that case there would be just one index, only on email?&lt;/p&gt;

&lt;p&gt;That's a broadly valid argument, but I'll put it this way: it's not worth it. If you don't use email as a Natural ID, then whether or not to create a unique index on email is a decision to make case by case. &lt;strong&gt;I'd say that for 95%+ of cases the answer is definitely yes, and there won't be any problems with it.&lt;/strong&gt; That said, for large write-heavy systems with a lot of data, this may create a certain overhead, &lt;strong&gt;but again, usually negligible at the scale of the system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And finally, regarding &lt;code&gt;MERGE&lt;/code&gt; / &lt;code&gt;INSERT ON CONFLICT&lt;/code&gt; operations. You can perfectly well do them not on the primary key, but on any constraint, for example, on a &lt;code&gt;UNIQUE&lt;/code&gt; constraint that you explicitly define in a migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Based on my experience, I can tell you one thing: remember that the scope of your not-knowing is by nature far larger than the scope of your "knowing". That's why it's very dangerous to build an assumption that a Natural ID, which seems unique to you for a given record in the moment, will make a good Primary Key.&lt;/p&gt;

&lt;p&gt;That said, it's worth acknowledging that &lt;strong&gt;the main advantage of a Natural ID is that it forces you to think about what invariants your data has in general.&lt;/strong&gt; And these invariants should give you insights into how to model your data access and storage patterns, for example, defining unique b+tree indexes for the email column.&lt;/p&gt;

&lt;p&gt;Remember: indexes and things like that can later be removed without consequences for the whole system. Changing primary keys, on the other hand, is a dead end.&lt;/p&gt;

</description>
      <category>database</category>
      <category>java</category>
      <category>software</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
