<?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: Rabinarayan Patra</title>
    <description>The latest articles on DEV Community by Rabinarayan Patra (@rabinarayanpatra).</description>
    <link>https://dev.to/rabinarayanpatra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866211%2F912ba316-ebe2-4423-82ef-f52f81e220c4.webp</url>
      <title>DEV Community: Rabinarayan Patra</title>
      <link>https://dev.to/rabinarayanpatra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rabinarayanpatra"/>
    <language>en</language>
    <item>
      <title>PostgreSQL 18 Temporal Foreign Keys with Spring Boot JPA</title>
      <dc:creator>Rabinarayan Patra</dc:creator>
      <pubDate>Thu, 21 May 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/rabinarayanpatra/postgresql-18-temporal-foreign-keys-with-spring-boot-jpa-lcc</link>
      <guid>https://dev.to/rabinarayanpatra/postgresql-18-temporal-foreign-keys-with-spring-boot-jpa-lcc</guid>
      <description>&lt;p&gt;PostgreSQL 18 shipped temporal foreign keys. The kind of feature SQL standards committees promised back in SQL:2011 and most database vendors quietly ignored. Now it's in mainline PG and the Java ecosystem has almost no tutorials on how to use it from Spring Boot.&lt;/p&gt;

&lt;p&gt;I went looking for "Spring Boot temporal foreign key" guides last week. The top results were 2018 Baeldung posts on hand-rolled &lt;code&gt;validFrom/validTo&lt;/code&gt; columns with zero database-level constraints. Plenty of &lt;code&gt;BETWEEN&lt;/code&gt; queries. Plenty of "remember to add an index". No one talks about PG18 yet. So I built a working example, hit every gotcha, and wrote it up.&lt;/p&gt;

&lt;p&gt;This post walks the full path: what temporal FKs actually solve, the PostgreSQL 18 syntax, how to map range columns in Hibernate, the Spring Data repository patterns that work, and the ON DELETE behavior that will absolutely surprise you if you skip the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem do temporal foreign keys solve?
&lt;/h2&gt;

&lt;p&gt;A temporal foreign key enforces that a child row's time range fits entirely inside its parent row's time range, at the database level, on every insert and update. That's the part regular foreign keys can't do.&lt;/p&gt;

&lt;p&gt;Take a classic example. An employee changes departments three times in five years. A project assignment references the employee. The business rule is: a project assignment can only exist during a period when the employee was actually employed.&lt;/p&gt;

&lt;p&gt;The hand-rolled approach looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;emp_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_from&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_to&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;project_assignments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;assignment_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;emp_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;assignment_start&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;assignment_end&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Spring shop I've worked at had a variation of this. And every one of them had bugs. Project assignments referencing employee IDs whose valid period ended six months ago. Manual &lt;code&gt;BETWEEN&lt;/code&gt; checks in service layers that someone forgot to update. Audit failures during quarterly reviews. The data model lied about what it was.&lt;/p&gt;

&lt;p&gt;PG18 fixes this at the constraint level. The constraint is the truth, not a service-layer hope.&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.amazonaws.com%2Fuploads%2Farticles%2Fn75q7atw3lhwbfzhq0ma.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn75q7atw3lhwbfzhq0ma.webp" alt="PostgreSQL 18 temporal foreign keys connecting employees with overlapping validity periods to project assignments" width="799" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How does PostgreSQL 18 implement WITHOUT OVERLAPS and PERIOD?
&lt;/h2&gt;

&lt;p&gt;PG18 introduces two new clauses: &lt;code&gt;WITHOUT OVERLAPS&lt;/code&gt; for primary and unique keys, and &lt;code&gt;PERIOD&lt;/code&gt; for foreign keys. Both rely on range types and GiST indexes under the hood.&lt;/p&gt;

&lt;p&gt;Here's the employees table redone the right way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;btree_gist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;emp_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;valid_period&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt; &lt;span class="k"&gt;WITHOUT&lt;/span&gt; &lt;span class="k"&gt;OVERLAPS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;btree_gist&lt;/code&gt; extension is required because the primary key mixes a regular &lt;code&gt;BIGINT&lt;/code&gt; column with a range column. PG needs a GiST index that can handle both, and &lt;code&gt;btree_gist&lt;/code&gt; provides the btree operator support inside GiST. If you forget it, the &lt;code&gt;CREATE TABLE&lt;/code&gt; will fail with a confusing operator error.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;valid_period&lt;/code&gt; column uses &lt;code&gt;daterange&lt;/code&gt;, one of PostgreSQL's built-in range types. You can also use &lt;code&gt;tstzrange&lt;/code&gt; for timestamp ranges or &lt;code&gt;int4range&lt;/code&gt; for numeric ranges. The constraint creates a GiST index automatically. Try to insert two rows for the same &lt;code&gt;emp_id&lt;/code&gt; with overlapping &lt;code&gt;valid_period&lt;/code&gt; values and PG rejects it.&lt;/p&gt;

&lt;p&gt;Now the project assignments table with a real temporal FK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;project_assignments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;assignment_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;emp_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;assignment_period&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PERIOD&lt;/span&gt; &lt;span class="n"&gt;assignment_period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PERIOD&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;PERIOD&lt;/code&gt; keyword tells PG that the last column is a range. The check is not equality. The check is containment: the parent's matching rows must, in combination, fully cover the child's range. If the employee's &lt;code&gt;valid_period&lt;/code&gt; ends June 1 2024 and the project assignment runs March 1 to August 1 2024, the insert fails because June 1 to August 1 has no parent row covering it.&lt;/p&gt;

&lt;p&gt;You can verify this with a deliberate bad insert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-01'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;project_assignments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emp_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assignment_period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Migration'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daterange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-03-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-08-01'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: insert or update on table "project_assignments" violates foreign key constraint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint catches it. No service code needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you map daterange columns in Hibernate?
&lt;/h2&gt;

&lt;p&gt;Hibernate 6 added native support for PostgreSQL range types through &lt;code&gt;PostgreSQLRangeJdbcType&lt;/code&gt;, but the cleanest path for a Spring Boot app is still the &lt;code&gt;hypersistence-utils&lt;/code&gt; library. It's the rebranded version of what most of us used as &lt;code&gt;hibernate-types-52&lt;/code&gt; for years, maintained by Vlad Mihalcea.&lt;/p&gt;

&lt;p&gt;Add the dependency to your &lt;code&gt;pom.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.hypersistence&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;hypersistence-utils-hibernate-63&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.10.7&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on Spring Boot 3.5+, you'll be on Hibernate 6.6, so use the &lt;code&gt;hypersistence-utils-hibernate-63&lt;/code&gt; artifact. Older Spring Boot 3.x versions use &lt;code&gt;-62&lt;/code&gt;. The version numbers track Hibernate ORM, not Spring Boot.&lt;/p&gt;

&lt;p&gt;Now the entity:&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.hypersistence.utils.hibernate.type.range.Range&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jakarta.persistence.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.hibernate.annotations.Type&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.math.BigDecimal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.time.LocalDate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"employees"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@IdClass&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EmployeeId&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Employee&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"emp_id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;empId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@Type&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PostgreSQLRangeType&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="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"valid_period"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"daterange"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validPeriod&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// getters, setters, constructors&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to flag.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@IdClass(EmployeeId.class)&lt;/code&gt; is needed because the primary key is composite. The &lt;code&gt;EmployeeId&lt;/code&gt; class is a plain Java record or class with the two ID fields and &lt;code&gt;equals&lt;/code&gt;/&lt;code&gt;hashCode&lt;/code&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="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;EmployeeId&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;empId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validPeriod&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Serializable&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@Type(PostgreSQLRangeType.class)&lt;/code&gt; annotation is what makes Hibernate emit and read &lt;code&gt;daterange&lt;/code&gt; correctly. The &lt;code&gt;columnDefinition = "daterange"&lt;/code&gt; part is what makes JPA's schema generation produce the right column type if you let JPA create the schema. In production, you should use Flyway or Liquibase, not JPA schema generation, but it's worth being explicit either way.&lt;/p&gt;

&lt;p&gt;You construct ranges 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="nc"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;closedOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDate&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="mi"&gt;2024&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="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDate&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="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;closedOpen&lt;/code&gt; matches PostgreSQL's &lt;code&gt;[)&lt;/code&gt; notation, which is the most common range form for temporal data. Half-open intervals make adjacent ranges easy to reason about: &lt;code&gt;[Jan 1, Jun 1)&lt;/code&gt; and &lt;code&gt;[Jun 1, Dec 1)&lt;/code&gt; are adjacent, not overlapping.&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.amazonaws.com%2Fuploads%2Farticles%2F21r8mq7ktydzlnpb6nmo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21r8mq7ktydzlnpb6nmo.webp" alt="Hibernate range type mapping: Java Range&amp;lt;LocalDate&amp;gt; through hypersistence-utils to PostgreSQL daterange" width="799" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you write the entity and repository?
&lt;/h2&gt;

&lt;p&gt;Spring Data JPA does most of the work, but you need a custom query for the overlap check at the application level. Database constraints catch invalid inserts, but the app still needs to ask "who was employed during this period?"&lt;/p&gt;

&lt;p&gt;The repository:&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="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;EmployeeRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EmployeeId&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&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="sh"&gt;"""
        SELECT *
        FROM employees
        WHERE emp_id = :empId
          AND valid_period &amp;amp;&amp;amp; :period
        """&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nativeQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findOverlapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"empId"&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;empId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"period"&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;period&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 &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator is PostgreSQL's range-overlap operator. You pass the range as a string in PG range literal form: &lt;code&gt;[2024-01-01,2024-06-01)&lt;/code&gt;. JPA doesn't have a native range-binding mechanism, so the string approach is the pragmatic move. If you want strong typing, Vlad's library offers &lt;code&gt;Range.toString()&lt;/code&gt; which formats correctly.&lt;/p&gt;

&lt;p&gt;Here's a service method that finds the employee record valid for a given date:&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;EmploymentService&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;EmployeeRepository&lt;/span&gt; &lt;span class="n"&gt;repo&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;EmploymentService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EmployeeRepository&lt;/span&gt; &lt;span class="n"&gt;repo&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAt&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;empId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;)&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;singleDay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[%s,%s]"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&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;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOverlapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;singleDay&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;findFirst&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;For the project assignment side, the insert just works. Hibernate sends the daterange, PG checks the temporal FK, and if the assignment period isn't fully covered by some employee period, the transaction rolls back.&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="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ProjectAssignment&lt;/span&gt; &lt;span class="nf"&gt;assign&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;empId&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;projectName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ProjectAssignment&lt;/span&gt; &lt;span class="n"&gt;pa&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;ProjectAssignment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmpId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProjectName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAssignmentPeriod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&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;assignmentRepo&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;pa&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;If the period extends past the employee's validity, you get a &lt;code&gt;DataIntegrityViolationException&lt;/code&gt; wrapping the PG error. Catch it where your error handling needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the gotchas with ON DELETE and temporal foreign keys?
&lt;/h2&gt;

&lt;p&gt;This is the one that will burn you if you skip the docs. PostgreSQL 18 does not support &lt;code&gt;CASCADE&lt;/code&gt;, &lt;code&gt;RESTRICT&lt;/code&gt;, &lt;code&gt;SET NULL&lt;/code&gt;, or &lt;code&gt;SET DEFAULT&lt;/code&gt; referential actions on temporal foreign keys. Only &lt;code&gt;NO ACTION&lt;/code&gt; is allowed.&lt;/p&gt;

&lt;p&gt;From the official PG18 CREATE TABLE docs: "In a temporal foreign key, this option is not supported." That sentence appears under every action except &lt;code&gt;NO ACTION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What does this mean in practice? If you delete an employee row that has dependent project assignments, PG raises a foreign key violation. You have to delete the assignments first, manually, in application code or in a database trigger you write yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This will fail at the second statement&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;emp_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: update or delete on table "employees" violates foreign key constraint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workaround patterns I've seen:&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.amazonaws.com%2Fuploads%2Farticles%2Flf70o6gjg63n22jsnegr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flf70o6gjg63n22jsnegr.webp" alt="Three workaround patterns for ON DELETE on temporal foreign keys: app-level delete, soft delete, period close" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first is application-level deletion. Spring service methods that delete child rows before parent rows, wrapped in a &lt;code&gt;@Transactional&lt;/code&gt; boundary. This is fine for small graphs, but it doesn't scale to deep hierarchies.&lt;/p&gt;

&lt;p&gt;The second is soft delete on the parent. Add a &lt;code&gt;deleted_at&lt;/code&gt; column, never actually delete rows, and let the temporal FK stay intact forever. Most enterprise systems do this anyway for audit reasons.&lt;/p&gt;

&lt;p&gt;The third is to flip the model: instead of deleting the parent, you close the parent's period by updating &lt;code&gt;valid_period&lt;/code&gt; to end at today's date. Future child inserts will fail. Existing child rows stay valid because their periods were covered when they were created.&lt;/p&gt;

&lt;p&gt;The third approach is closest to the spirit of temporal modeling. You don't lose history. You just declare "this record stopped being valid at this point" and let the constraints enforce that going forward.&lt;/p&gt;

&lt;p&gt;If you absolutely need cascading deletes, you can write a trigger that does the deletion manually before the parent delete fires. But at that point you're rebuilding what the standard wanted to give you, and the SQL spec authors decided cascade semantics on overlapping periods were too ambiguous to specify. They might be right.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you NOT use temporal foreign keys?
&lt;/h2&gt;

&lt;p&gt;Temporal foreign keys are powerful, and like most powerful features they're easy to overuse. I've seen teams reach for them in places where regular FKs would be simpler and just as correct.&lt;/p&gt;

&lt;p&gt;Skip temporal FKs when:&lt;/p&gt;

&lt;p&gt;You only need audit history. If the question is "what changed when?" and not "what was the state of the world on date X?", a separate audit log table with regular FKs is simpler. Frameworks like Hibernate Envers handle this well.&lt;/p&gt;

&lt;p&gt;The child rows don't have an independent time dimension. If a project assignment is just "associated with employee 1 forever", you don't need temporal FKs. A regular FK with a &lt;code&gt;created_at&lt;/code&gt; timestamp is fine.&lt;/p&gt;

&lt;p&gt;You're modeling a single current state. CRUD apps where the latest row is the only thing that matters don't need temporal modeling. Add a &lt;code&gt;valid_period&lt;/code&gt; column and you've created complexity you'll have to pay for in every query.&lt;/p&gt;

&lt;p&gt;You can't pay the GiST index cost. GiST indexes are slower for point lookups than btree indexes. For high-throughput single-row reads, you'll feel it. Benchmark first.&lt;/p&gt;

&lt;p&gt;When the model genuinely is temporal, the constraint-enforced version is a huge improvement over the hand-rolled version. The number of bugs you avoid by having PG check every insert is real. The cost of writing the queries against ranges instead of timestamps is real too, but it's a one-time cost. The bugs are forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;PostgreSQL 18 temporal foreign keys are one of the most under-discussed major-version features I've seen in a while. The Spring Boot ecosystem has barely caught up. If you're modeling employment, contracts, pricing tiers, policies, or anything else where state has duration, the new &lt;code&gt;WITHOUT OVERLAPS&lt;/code&gt; and &lt;code&gt;PERIOD&lt;/code&gt; clauses are worth learning even if you don't ship them tomorrow.&lt;/p&gt;

&lt;p&gt;The piece I want more people to understand: this changes what your database can be the source of truth for. Before PG18, "this assignment must fall inside an employment period" was a service-layer concern that drifted out of sync. Now it's a constraint. The database tells the truth.&lt;/p&gt;

&lt;p&gt;For more on the PG18 release, see the &lt;a href="https://www.postgresql.org/docs/release/18.0/" rel="noopener noreferrer"&gt;PostgreSQL 18.0 release notes&lt;/a&gt;, the &lt;a href="https://neon.com/postgresql/postgresql-18/temporal-constraints" rel="noopener noreferrer"&gt;Neon writeup on temporal constraints&lt;/a&gt;, and the &lt;a href="https://www.postgresql.org/docs/current/sql-createtable.html" rel="noopener noreferrer"&gt;CREATE TABLE syntax docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide" rel="noopener noreferrer"&gt;Spring Boot Testcontainers Guide&lt;/a&gt;. Real PG18 in tests beats mocked databases for catching constraint bugs early.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide" rel="noopener noreferrer"&gt;Hibernate Lazy Init Guide&lt;/a&gt;. The other Hibernate gotcha that bites every Spring team.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide" rel="noopener noreferrer"&gt;PgBouncer Survival Guide&lt;/a&gt;. Connection pooling matters more when GiST indexes are doing real work.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>java</category>
      <category>postgres</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Amazon S3 Files: AWS Just Turned Object Storage Into a File System</title>
      <dc:creator>Rabinarayan Patra</dc:creator>
      <pubDate>Thu, 09 Apr 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/rabinarayanpatra/amazon-s3-files-aws-just-turned-object-storage-into-a-file-system-8md</link>
      <guid>https://dev.to/rabinarayanpatra/amazon-s3-files-aws-just-turned-object-storage-into-a-file-system-8md</guid>
      <description>&lt;p&gt;I've been running EFS-to-S3 sync jobs for two years. Cron schedules, lifecycle policies, rsync scripts that break every time someone changes a directory structure. All because S3 couldn't speak file system.&lt;/p&gt;

&lt;p&gt;That changed this week.&lt;/p&gt;

&lt;p&gt;On April 7, 2026, AWS announced &lt;a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;, a feature that lets you mount any S3 bucket as a shared NFS file system. No gateway. No third-party tool. No data copies. Your applications read and write files, and S3 stores them. That's it.&lt;/p&gt;

&lt;p&gt;And quietly, on April 6, AWS also started &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/04/s3-default-bucket-security-setting/" rel="noopener noreferrer"&gt;disabling SSE-C encryption by default&lt;/a&gt; on all new S3 buckets. If you're managing encryption keys manually, that one needs your attention too.&lt;/p&gt;

&lt;p&gt;Let me walk through both changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon S3 Files and how does it work?
&lt;/h2&gt;

&lt;p&gt;Amazon S3 Files gives S3 buckets a fully-featured file system interface using NFS v4.2. You can mount a bucket on EC2, Lambda, EKS, ECS, Fargate, or AWS Batch and interact with your data using standard file operations: &lt;code&gt;open()&lt;/code&gt;, &lt;code&gt;read()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;. No SDK. No API calls. Just a mount point.&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.amazonaws.com%2Fuploads%2Farticles%2Flch1l9n3z8m9o0ajoxcb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flch1l9n3z8m9o0ajoxcb.webp" alt="S3 Files Architecture Overview - Source: AWS Blog" width="800" height="466"&gt;&lt;/a&gt;&lt;em&gt;Architecture overview of Amazon S3 Files. Source: &lt;a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/" rel="noopener noreferrer"&gt;AWS News Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;S3 is now the first and only cloud object store that provides native file system access while keeping all data in object storage. Your objects don't move to a separate file system. They stay in S3 with all the durability, lifecycle policies, and access controls you already have.&lt;/p&gt;

&lt;p&gt;S3 Files is generally available in 34 AWS Regions as of launch day.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stage and commit model
&lt;/h3&gt;

&lt;p&gt;This is the part that surprised me. Instead of translating every file write into an immediate S3 PUT, S3 Files batches changes and commits them to S3 roughly every 60 seconds. AWS borrowed this concept from version control.&lt;/p&gt;

&lt;p&gt;Here's what that means in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You write a file through the NFS mount&lt;/li&gt;
&lt;li&gt;The write lands in a caching layer (built on EFS infrastructure)&lt;/li&gt;
&lt;li&gt;S3 Files aggregates writes within a 60-second window&lt;/li&gt;
&lt;li&gt;Multiple writes to the same file become a single S3 PUT&lt;/li&gt;
&lt;li&gt;The committed object appears in S3 with full consistency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This batching has two practical benefits. First, it cuts your S3 request costs because ten rapid writes to the same file become one PUT, not ten. Second, if you're using S3 versioning, you don't end up with ten versions of a file that changed in under a minute.&lt;/p&gt;

&lt;p&gt;But it also means there's a ~60-second lag before file changes are visible on the S3 side. If your workflow needs immediate S3 API visibility of written data, you need to account for that delay.&lt;/p&gt;

&lt;p&gt;When there's a conflict (say, someone writes to a file through NFS while another process updates the same object via the S3 API), S3 remains authoritative. The file-side version gets moved to a &lt;code&gt;lost+found&lt;/code&gt; directory with metrics for visibility. No silent data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  The caching layer under the hood
&lt;/h3&gt;

&lt;p&gt;When you create an S3 Files file system, AWS provisions a caching layer backed by EFS infrastructure. This cache holds three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recently read files&lt;/strong&gt; : Hot reads come from cache with sub-millisecond latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recently written files&lt;/strong&gt; : Staged writes waiting for the next commit cycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt; : Directory listings, file attributes, timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small file reads are served entirely from the cache. Large file reads (over 1MB) stream directly from S3 and don't incur S3 Files charges. The aggregate read throughput can reach multiple terabytes per second.&lt;/p&gt;

&lt;p&gt;This design is what makes S3 Files cost-effective. You pay file system rates ($0.30/GB-month) only on the data that's actively cached. A petabyte bucket where only 500GB is actively read? You're paying S3 rates for the petabyte and file system rates for the 500GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set up S3 Files on an EC2 instance?
&lt;/h2&gt;

&lt;p&gt;The setup is straightforward. Here's the full flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: Create an S3 Files file system linked to your bucket&lt;/span&gt;
aws s3api create-file-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-data-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file-system-name&lt;/span&gt; my-fs

&lt;span class="c"&gt;# Step 2: Get the mount target DNS name&lt;/span&gt;
aws s3api describe-file-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-data-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file-system-name&lt;/span&gt; my-fs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'FileSystem.MountTargets[0].DnsName'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text

&lt;span class="c"&gt;# Step 3: Mount on your EC2 instance&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; nfs4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;nfsvers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.2,rsize&lt;span class="o"&gt;=&lt;/span&gt;1048576,wsize&lt;span class="o"&gt;=&lt;/span&gt;1048576 &lt;span class="se"&gt;\&lt;/span&gt;
  fs-mount-target.efs.us-east-1.amazonaws.com:/ &lt;span class="se"&gt;\&lt;/span&gt;
  /mnt/s3data

&lt;span class="c"&gt;# Step 4: Use it like any filesystem&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/s3data
&lt;span class="nb"&gt;cp &lt;/span&gt;local-file.csv /mnt/s3data/uploads/
&lt;span class="nb"&gt;cat&lt;/span&gt; /mnt/s3data/reports/quarterly.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once mounted, every application on that instance can access S3 data through normal file I/O. Python scripts, Java apps, shell scripts, legacy C++ binaries. Nothing needs to know it's talking to S3.&lt;/p&gt;

&lt;p&gt;For persistent mounts across reboots, add it to &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/fstab entry for S3 Files
&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;-&lt;span class="n"&gt;mount&lt;/span&gt;-&lt;span class="n"&gt;target&lt;/span&gt;.&lt;span class="n"&gt;efs&lt;/span&gt;.&lt;span class="n"&gt;us&lt;/span&gt;-&lt;span class="n"&gt;east&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="n"&gt;amazonaws&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;:/ /&lt;span class="n"&gt;mnt&lt;/span&gt;/&lt;span class="n"&gt;s3data&lt;/span&gt; &lt;span class="n"&gt;nfs4&lt;/span&gt; &lt;span class="n"&gt;nfsvers&lt;/span&gt;=&lt;span class="m"&gt;4&lt;/span&gt;.&lt;span class="m"&gt;2&lt;/span&gt;,&lt;span class="n"&gt;rsize&lt;/span&gt;=&lt;span class="m"&gt;1048576&lt;/span&gt;,&lt;span class="n"&gt;wsize&lt;/span&gt;=&lt;span class="m"&gt;1048576&lt;/span&gt;,&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;netdev&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where does S3 Files beat EFS (and where does it not)?
&lt;/h2&gt;

&lt;p&gt;This is the question I've been testing all week. S3 Files isn't a drop-in EFS replacement for every workload, but for specific patterns it's clearly better.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 Files wins
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Why S3 Files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large datasets, small active working set&lt;/td&gt;
&lt;td&gt;Pay S3 rates on cold data, file system rates only on hot data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy app migration to S3&lt;/td&gt;
&lt;td&gt;Zero code changes needed, just mount and go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI/ML training data pipelines&lt;/td&gt;
&lt;td&gt;Read training data as files, store as S3 objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic AI workloads&lt;/td&gt;
&lt;td&gt;Shared workspace across multiple compute instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-service data sharing&lt;/td&gt;
&lt;td&gt;Multiple EKS pods or Lambda functions reading the same dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  EFS still wins
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Why EFS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All data is hot, constant read/write&lt;/td&gt;
&lt;td&gt;EFS avoids the commit delay and S3 request overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-second write visibility needed&lt;/td&gt;
&lt;td&gt;S3 Files has a ~60-second commit lag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows workloads (SMB)&lt;/td&gt;
&lt;td&gt;S3 Files only supports NFS, no SMB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard link requirements&lt;/td&gt;
&lt;td&gt;S3 Files doesn't support hard links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bucket exceeds 50 million objects&lt;/td&gt;
&lt;td&gt;AWS warns about performance at this scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Pricing comparison
&lt;/h3&gt;

&lt;p&gt;The pricing math depends entirely on your access pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files cached storage&lt;/strong&gt; : $0.30/GB-month (only for actively cached data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files reads (small files)&lt;/strong&gt;: $0.03/GB from cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files reads (large files, 1MB+)&lt;/strong&gt;: $0 from S3 Files (standard S3 GET charges apply)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 Files writes&lt;/strong&gt; : $0.06/GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare that to EFS Performance-optimized at $0.30/GB for standard storage and $0.03/GB for reads. The difference shows up at scale: if you have 10TB in a bucket but only touch 200GB regularly, S3 Files costs a fraction of what an equivalent EFS setup would cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the gotchas you should know before adopting S3 Files?
&lt;/h2&gt;

&lt;p&gt;I ran into a few things during my initial testing that are worth flagging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 60-second commit window is real.&lt;/strong&gt; If you write a file via NFS and immediately try to read it through the S3 API (using &lt;code&gt;aws s3 cp&lt;/code&gt; or a direct GET), it won't be there yet. Your application logic needs to handle this. For workflows that do writes via NFS and reads via S3 API, consider adding a short wait or checking for object existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NFS file locks don't protect against S3 API access.&lt;/strong&gt; If you lock a file through NFS, that lock only applies to other NFS clients. Someone using the S3 API directly can still modify the object. This isn't a bug. It's how the boundary between file system and object store works. But it can bite you if mixed access isn't on your radar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 50-million object warning is something to watch.&lt;/strong&gt; AWS recommends caution when a mounted bucket contains more than 50 million objects. Directory listings and metadata operations can slow down at that scale. If you're dealing with buckets that large, consider using S3 prefixes to scope your mount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No pNFS, Kerberos, or nconnect support.&lt;/strong&gt; If your NFS setup depends on parallel NFS, Kerberos authentication, NFSv4 data retention, or the &lt;code&gt;nconnect&lt;/code&gt; mount option, those aren't available yet at GA. Standard NFS v4.2 features work fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SMB is not supported.&lt;/strong&gt; Windows workloads that need file system access to S3 still need FSx or a gateway solution.&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.amazonaws.com%2Fuploads%2Farticles%2Fcvslwq19u9pa48apnm8v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvslwq19u9pa48apnm8v.webp" alt="S3 Files Technical Comparison Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did AWS disable SSE-C encryption by default?
&lt;/h2&gt;

&lt;p&gt;This change flew under the radar next to S3 Files, but it affects every new bucket created after April 6, 2026.&lt;/p&gt;

&lt;p&gt;SSE-C (Server-Side Encryption with Customer-Provided Keys) lets you bring your own encryption key on every PUT and GET request. S3 encrypts and decrypts using your key but never stores it. The idea is maximum control. The reality is operational risk. Lose the key, lose the data forever. AWS can't recover it for you. There's no "forgot my password" option.&lt;/p&gt;

&lt;p&gt;AWS KMS solved this years ago with customer-managed keys (CMKs) that give you full ownership and control, plus key rotation, auditing through CloudTrail, and recovery options. For most workloads, KMS does everything SSE-C does, minus the footgun.&lt;/p&gt;

&lt;p&gt;So AWS made SSE-C opt-in instead of opt-out. Here's how the rollout works:&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes for new buckets
&lt;/h3&gt;

&lt;p&gt;Every new general-purpose S3 bucket created after April 6, 2026 has SSE-C disabled by default. If you try to upload an object with SSE-C headers, you'll get an access denied error unless you explicitly enable SSE-C first.&lt;/p&gt;

&lt;p&gt;To enable SSE-C on a new bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Explicitly allow SSE-C on a new bucket&lt;/span&gt;
aws s3api put-bucket-encryption &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-new-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-side-encryption-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      },
      "BucketKeyEnabled": false
    }],
    "AllowSSEC": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What changes for existing buckets
&lt;/h3&gt;

&lt;p&gt;This is the part that might catch people off guard. AWS is also disabling SSE-C on existing buckets that have &lt;strong&gt;zero SSE-C encrypted objects&lt;/strong&gt;. If you created a bucket, never used SSE-C, but your automation code includes SSE-C headers "just in case," those writes will start failing.&lt;/p&gt;

&lt;p&gt;Existing buckets that actually contain SSE-C objects? No changes. AWS won't touch those.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who this affects
&lt;/h3&gt;

&lt;p&gt;If you're using AWS KMS (SSE-KMS) or S3-managed keys (SSE-S3) for encryption, this change does nothing to you. Your buckets already don't use SSE-C.&lt;/p&gt;

&lt;p&gt;If you're one of the teams still on SSE-C, you'll want to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit which buckets actually use SSE-C (&lt;code&gt;aws s3api get-bucket-encryption&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Plan a migration to KMS for buckets that don't strictly need SSE-C&lt;/li&gt;
&lt;li&gt;Explicitly re-enable SSE-C on new buckets where it's genuinely required&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rollout covers 37 AWS Regions, including GovCloud and China regions, and will complete over the next few weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do these changes tell us about where S3 is heading?
&lt;/h2&gt;

&lt;p&gt;If I look at S3 Files and the SSE-C default together, they tell the same story: AWS is reducing the reasons you'd reach for anything other than S3.&lt;/p&gt;

&lt;p&gt;Need file system access? You used to need EFS plus sync scripts. Now you mount S3 directly. Need encryption with your own keys? You used to reach for SSE-C. Now AWS is steering you toward KMS, which handles key management for you.&lt;/p&gt;

&lt;p&gt;S3 now stores over 500 trillion objects and handles roughly 200 million requests per second. It turned 20 years old last month. And instead of letting it coast, AWS gave it the most significant capability upgrade since S3 Intelligent-Tiering launched in 2018.&lt;/p&gt;

&lt;p&gt;For my own projects, I'm already replacing two EFS-backed data pipelines with S3 Files mounts. The sync cron jobs are gone. The drift alerts are gone. One mount point, one storage bill, and a 60-second commit window I can easily live with.&lt;/p&gt;

&lt;p&gt;If you've been running parallel storage systems just to get file access to your S3 data, this week is a good week to rethink that architecture.&lt;/p&gt;

&lt;p&gt;For the full details, see the &lt;a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/" rel="noopener noreferrer"&gt;official S3 Files announcement&lt;/a&gt;, the &lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;S3 Files product page&lt;/a&gt;, and the &lt;a href="https://aws.amazon.com/blogs/storage/advanced-notice-amazon-s3-to-disable-the-use-of-sse-c-encryption-by-default-for-all-new-buckets-and-select-existing-buckets-in-april-2026/" rel="noopener noreferrer"&gt;SSE-C security default announcement&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/ai-driven-anomaly-detection-security"&gt;AI-Driven Anomaly Detection for Security&lt;/a&gt; - How cloud infrastructure and AI work together for real-time threat detection.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/implementing-outbox-pattern-cdc-microservices"&gt;Implementing the Outbox Pattern with CDC in Microservices&lt;/a&gt; - Storage design patterns that affect reliability in distributed systems.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blogs/the-day-react-patch-broke-the-internet"&gt;The Day a React Patch Broke the Internet&lt;/a&gt; - Another deep dive into an infrastructure event that caught everyone off guard.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>news</category>
    </item>
  </channel>
</rss>
