skip to content

Enum & Date/Time Mapping

Mapping enums and java.time types without planting refactoring landmines. The ORDINAL-vs-STRING question is an interview classic because ORDINAL breaks silently the day someone reorders the enum.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

6

How does JPA's @Enumerated annotation store a Java enum in the database, what is its default, and what is the practical difference between EnumType.ORDINAL and EnumType.STRING?

level: juniorimportance: must knowfreq 72%

answer

  1. Default = ORDINAL (the risky one)
  2. ORDINAL stores position; reorder rewrites meaning silently
  3. STRING stores name(); safe to reorder, breaks on rename
  4. STRING = readable in ad-hoc SQL
  5. Hibernate 6: check constraint + NAMED_ENUM option

basics

~20 s

By default (ORDINAL) JPA stores the enum constant's declaration index as a number, so reordering or inserting constants silently remaps existing rows. EnumType.STRING stores the constant's name, which is stable under reordering and readable, but breaks if you rename a constant.

solid answer

~50 s

`@Enumerated` controls how an enum attribute is persisted. Its **default is `EnumType.ORDINAL`**, which stores `Enum.ordinal()` — the zero-based position in the declaration list. `EnumType.STRING` stores `Enum.name()`. ORDINAL is compact and indexes well, but the stored value depends on **source-code order**. Insert a constant in the middle or reorder the list and every existing row silently means something else — no error, no migration, wrong data. That is the single most-cited enum trap in JPA. STRING stores `"ACTIVE"`, which is self-describing in the database, safe under reordering, and fails loudly (`IllegalArgumentException` on read) if a name disappears. Its risks are renames — which are just as breaking as reordering under ORDINAL, but a rename is a visible refactor — plus a slightly larger column. Practical rule: always write `@Enumerated(EnumType.STRING)` explicitly unless the schema is legacy or the column is genuinely hot; if you need decoupling from both order and names, map an explicit stable code instead.

code

java · 10 lines
java
public enum OrderStatus { NEW, PAID, SHIPPED }

@Entity
public class Order {
    @Id @GeneratedValue private Long id;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", length = 20, nullable = false)
    private OrderStatus status;
}

go deeper

for a junior

State the default (ORDINAL), what each option stores, and the reorder trap with a concrete example. Recommend STRING explicitly.

for a middle

Add the tradeoffs: column width and index size, the rename hazard under STRING, and the lack of database-side validation without a constraint.

for a senior

Discuss migrating an existing ORDINAL column safely, Hibernate 6's check constraint and native-enum support, and when an explicit stable code beats both options.

for a principal

Treat the stored representation as a long-lived contract shared with reports, exports and other services; set a codebase rule, and decide whether enums or a lookup table should own the domain vocabulary.

## What @Enumerated does A Java enum has two natural string/number representations: `name()` (the constant's identifier, e.g. `SHIPPED`) and `ordinal()` (its zero-based position in the declaration, e.g. `2`). JPA's `@Enumerated` picks which one is written to the column. ```java public enum OrderStatus { NEW, PAID, SHIPPED } // ordinals 0, 1, 2 @Entity class Order { @Enumerated(EnumType.STRING) private OrderStatus status; // stores 'NEW' / 'PAID' / 'SHIPPED' } ``` Omitting `@Enumerated` entirely, or writing `@Enumerated(EnumType.ORDINAL)`, stores the integer. **ORDINAL is the specification default**, which is the root of most of the pain — the risky option is the one you get by doing nothing. ## Why ORDINAL is fragile Ordinals are positional, and positions are a property of the *source file*, not of the data. Suppose production holds thousands of rows written with the enum above, and a developer adds a status: ```java public enum OrderStatus { NEW, PENDING_PAYMENT, PAID, SHIPPED } // 0,1,2,3 ``` Every row that stored `1` meant `PAID`; it now reads as `PENDING_PAYMENT`. Every `2` was `SHIPPED` and now reads as `PAID`. Nothing throws. Nothing logs. Reports, state machines and business rules quietly operate on wrong values, and the damage is discovered days later. The same happens on any reorder, including a well-meaning alphabetical sort or an IDE refactor. Deleting a constant is comparatively kind: rows holding the now-out-of-range index fail on read. Mitigations exist — a comment saying "append only, never reorder", an architecture test asserting the ordinal of each constant, an explicit code column — but they all rely on discipline where STRING relies on the data itself. ## Why STRING is the usual default - Stored values are **self-describing**: someone reading `order` in psql sees `SHIPPED`, not `2`. Ad-hoc SQL, dashboards and support queries all get easier. - **Reordering and inserting constants are free.** - Failures are **loud**: reading a name with no matching constant throws rather than silently mapping to another value. Costs, stated honestly: - A wider column (`VARCHAR(20)` vs `SMALLINT`) and a wider index. On a hot, huge table this can matter; on most tables it does not. - **Renaming a constant breaks existing rows.** The refactor is visible in a diff, but it still needs an `UPDATE ... SET status = 'NEW_NAME' WHERE status = 'OLD_NAME'` migration. - Nothing constrains the column to valid values unless you also add a `CHECK` constraint or a lookup table. ## What Hibernate adds Hibernate 6 tightened enum handling: for ORDINAL mappings it prefers a small integer type (`TINYINT`/`SMALLINT`) rather than `INTEGER`, and its schema generation emits a `CHECK` constraint listing the permitted values, so out-of-range data is rejected by the database. Hibernate 6.2 also added `@JdbcTypeCode(SqlTypes.NAMED_ENUM)` (and `NAMED_ORDINAL`), which maps to a **native database enum type** where one exists — for example a PostgreSQL `CREATE TYPE ... AS ENUM` — giving readable values plus database-side validation. That is a Hibernate extension, not portable JPA. If you need full independence from *both* declaration order and constant names, the standard answer is to store an explicit, deliberately assigned code (`"SHP"`, `10`) that lives in the enum as a field and is converted on the way in and out. That decouples the wire format from the Java identifier entirely, at the price of one more moving part. ## Interview framing Say: default is ORDINAL, prefer STRING, explain the reorder failure with a concrete before/after, then acknowledge the storage cost and the rename caveat. That sequence — default, failure mode, tradeoff — is exactly what the question is probing.

  • What is the safest way to add a new constant to an enum already persisted with EnumType.ORDINAL?
    Append it at the end of the declaration list so no existing ordinal shifts, and never reorder or delete earlier constants. That keeps stored data valid, but it is a convention enforced only by discipline — which is why teams often add a test asserting each constant's ordinal, or migrate the column to STRING (or to an explicit code) at the first opportunity.
  • How would you decouple the stored value from both the constant order and the constant name?
    Give each constant an explicit, deliberately chosen code as a field — `SHIPPED("SHP")` — and persist that code rather than the ordinal or the name, converting in both directions. Renaming the Java constant or reordering the enum then has no effect on stored data. The cost is an extra mapping component and the need to guarantee codes are never reused.
  • Does EnumType.STRING protect you from all schema drift?
    No. It removes the reorder hazard, but renaming a constant still orphans existing rows, which then fail on read with an IllegalArgumentException. Deleting a constant that still exists in data has the same effect. STRING converts silent corruption into a loud failure — a large improvement, but still something a migration must handle.

saying these in an interview costs you the question

  • Saying STRING is the JPA default
  • Believing ORDINAL is safe as long as you 'just remember' not to reorder
  • Claiming reordering constants under ORDINAL causes an error at read time
  • Assuming STRING makes the column self-validating without a CHECK constraint or native enum type
  • Treating the storage difference between SMALLINT and VARCHAR(20) as decisive on every table

context

open as a page

When mapping date and time attributes with JPA/Hibernate, how do java.time types Instant, LocalDate, LocalDateTime, OffsetDateTime and ZonedDateTime differ in what they store, and how do you choose between them?

level: middleimportance: must knowfreq 56%

basics

~20 s

LocalDate maps to DATE, LocalTime to TIME, LocalDateTime to a timestamp without zone (wall-clock, no instant), Instant to a point on the timeline, and OffsetDateTime/ZonedDateTime carry an offset that survives only with a with-time-zone column or explicit offset storage. Use Instant for events, LocalDate/LocalDateTime for calendar facts.

open as a page

What is the JPA @Temporal annotation for, when is it required, and why should it not appear on attributes typed with the java.time API?

level: middleimportance: should knowfreq 36%

basics

~20 s

@Temporal exists only for the legacy types java.util.Date and java.util.Calendar, which are ambiguous: it tells the provider whether to map DATE, TIME or TIMESTAMP. java.time types already carry that meaning, so @Temporal is unnecessary and not permitted on them.

open as a page

What does the Hibernate configuration property hibernate.jdbc.time_zone do, why do teams set it to UTC, and what must you be careful about when introducing it into a system that already has data?

level: seniorimportance: should knowfreq 32%

basics

~20 s

It tells Hibernate which time zone to pass to JDBC when binding and reading timestamps, instead of the JVM default. Setting it to UTC makes stored values independent of the server's zone. Introducing it later re-interprets existing rows, so it needs a data migration, not just a config flip.

open as a page

A production table stores an enum attribute as ordinal integers and the team now needs to insert a new constant in the middle of the enum and move the column to string storage. How do you carry that out safely, and how would you detect damage if someone had already reordered the constants?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Freeze the current ordinal-to-name mapping from the old source, add a new string column, backfill it by mapping each stored ordinal through that frozen table in SQL, switch the mapping to EnumType.STRING, then drop the integer column. To detect prior damage, reconstruct the mapping from git history and cross-check row counts and timelines against business expectations.

open as a page

You are setting the convention for how a multi-service system persists dates and times through JPA/Hibernate entities. How do you decide what each attribute stores — UTC instants, local wall-clock plus a zone, or plain calendar dates — and what do you standardise across the codebase?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Classify each attribute first: a past moment stores an Instant in UTC; a calendar fact stores LocalDate; a future local commitment stores wall-clock plus an explicit zone id so later zone-rule changes stay correct. Then standardise UTC everywhere in storage, conversion only at edges, and one mapping style per meaning.

open as a page