skip to content

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