An entity is a noun in your domain that has its own identity, its own lifecycle, and enough independent attributes to justify a row rather than a column. If a concept can change state over time, gets referenced from multiple places, or can exist before all its details are known, it has earned a table. If it only ever describes something else, it stays an attribute.

Quick answer: Promote a noun to an entity when it passes three tests: it needs its own identity (an ID), it has its own lifecycle (created, updated, and deleted on its own schedule), and it carries attributes that matter independently of the thing it's attached to. If it fails any of those, it's an attribute — not a table.

This sounds like a technical distinction that only database architects should care about. It isn't. Every time a PM writes "the user can add multiple addresses" or "tickets have a status" in a PRD, they've made an entity decision — usually without noticing. Get it wrong and you either over-engineer a simple form or under-engineer a feature that quietly breaks the moment the business asks a slightly harder question of the data.

What Is an Entity, Really? The Lifecycle-Identity-Attributes Test

A noun deserves its own entity when it independently satisfies three conditions: it has an identity that persists even if every attribute changes, a lifecycle with its own create/update/delete timeline, and attributes that are meaningful on their own rather than as decoration on another object. Miss one of the three and it's almost always an attribute in disguise.

This isn't a new idea — it's the same distinction Eric Evans formalized in Domain-Driven Design (2003) between an Entity (defined by a thread of continuity and identity, not by its attributes) and a Value Object (defined entirely by its attributes, interchangeable if two instances have the same values). A Money amount of $50 is a value object; two $50s are identical and interchangeable. A Customer is an entity — two customers named "Alex Chen" are not the same customer.

Run the three tests one at a time:

  • Identity: Would you need to tell two instances apart even if every field matched? A Product named "Blue Widget" priced at $9.99 might exist in two warehouses — you still need to know which one shipped. That's identity.
  • Lifecycle: Does the thing get created, updated, or archived on a timeline separate from its parent? An Order is placed, paid, shipped, and returned independently of the Customer who placed it.
  • Independent attributes: Does it carry enough of its own data — timestamps, owners, sub-fields — that squeezing it into a column would lose information?

If a noun clears all three, model it as an entity with its own primary key. If it only clears one or two, it's very likely an attribute, or at most a value object embedded in a larger row. For the underlying framework this test builds on, our complete guide to data modeling walks through the full progression from domain language to schema.

Address as a String vs. Address as an Entity: A Worked Example

Whether Address deserves its own table depends on what your product actually does with it, not on how many lines the address has. A one-time contact form can store address as a single text field forever. A logistics, billing, or multi-location product almost always needs Address as a first-class entity with history, validation, and reuse across records.

The tell isn't complexity — a fully parsed address with street, city, state, and postal code can still be a value object stored as a JSON blob or a set of columns on the parent row, with no identity of its own. What forces promotion to a true entity is reuse and change over time:

SignalString field / embedded value object is finePromote Address to its own entity
ReuseAddress only ever belongs to one recordA customer has multiple shipping and billing addresses
HistoryYou only need the current valueYou must know what address an order shipped to a year ago, even after the customer moved
ValidationFree text, no geocodingAddress needs to be geocoded, deduplicated, or verified against a carrier API
RelationshipsNothing else references itAddresses are shared across accounts (e.g., a company HQ used by many contacts)
QueryingYou never search or filter by address independentlyYou need "all customers within this ZIP code" as a first-class query

A consumer app collecting a shipping address at checkout can usually get away with a JSON value object embedded in the Order row — it's a snapshot, not a living record. A B2B logistics platform where the same warehouse address is referenced by hundreds of shipments, and where an address correction needs to propagate or be tracked historically, needs Address as its own table with a customer_addresses join table pointing to it.

This is the same judgment call we describe in Data Model Is the Product: Draw the ERD First — the schema shape you choose here isn't a backend implementation detail, it's a product decision about what questions the business will be able to ask later.

The question that actually settles it

Ask: "If this address changes, does anything else in the system need to know it changed, or does it just need to know the new value?" If the answer involves history, auditing, or multiple owners, you need an entity. If the answer is "just overwrite it," a field is enough.

Status: The Classic Trap of Modeling State as Free Text

Status is the most common attribute PMs leave as a plain string or enum column when it should be modeled as its own entity — or at minimum, backed by a history table. The giveaway is that status has a real lifecycle of its own: it transitions through defined states, at specific times, usually caused by a specific actor. That's the textbook definition of an entity, not a label.

Picture a status VARCHAR(20) column on a Ticket table storing values like "open", "in_progress", "closed". It looks harmless in a demo. Then product asks: "How long do tickets sit in review before they're closed?" or "Who moved this back to open, and why?" — and the answer is nowhere in the schema, because overwriting a string column destroys the very history that was asked about.

Modeling choiceWhat you can answerWhat breaks
status as a free-text or enum columnWhat is the current state?Typos ("Open" vs "open"), no timestamp of transitions, no record of who changed it, no duration analysis
status_history entity (status, changed_by, changed_at, from_state, to_state)Current state, full transition timeline, cycle time per stage, who approved whatSlightly more schema and query complexity upfront

This exact problem is why data warehousing has a named pattern for it: Ralph Kimball's Slowly Changing Dimension (SCD) Type 2, which tracks every change to an attribute with effective-dated rows instead of overwriting the value in place. Kimball's dimensional modeling method has been the standard reference for this class of problem across the data warehousing industry for well over two decades — status is exactly the kind of field it was designed to protect.

You don't need a full SCD pipeline for every PRD. The lightweight version is a status_transitions table with entity_id, from_status, to_status, changed_by, and changed_at. It costs one join. It buys you every analytics question a stakeholder will eventually ask about cycle time, bottlenecks, and accountability — the same questions a systems thinking lens would tell you to expect, since status transitions are usually the feedback loop the business actually wants visibility into.

Why "just add more enum values" doesn't fix it

Teams often respond to this trap by adding more granular enum values — "in_review_by_legal", "in_review_by_finance" — instead of modeling the transition. That solves naming, not history. You still can't answer "how long," "how many times," or "who," because an enum column has no concept of when it changed. Only a row with a timestamp does.

The Entity-or-Attribute Checklist

Before adding a new noun to a data model, run it through a short checklist rather than a gut call. The checklist exists because "does this feel like its own thing" is a bad heuristic under deadline pressure — teams reliably default to whatever is fastest to type, which is almost always a string column.

This isn't a new invention — it operationalizes the distinction Peter Chen introduced in his 1976 paper "The Entity-Relationship Model — Toward a Unified View of Data," which is still the basis for how relational schemas are taught fifty years later. Chen's core insight was that entities and relationships are structurally different things, and conflating them is where most schema regret starts.

  1. Does it need its own identifier that outlives any single attribute value? (An address that gets corrected is still "the same address.")
  2. Does it have a lifecycle event — created, approved, expired, archived — that happens on a different schedule than its parent record?
  3. Is it referenced from more than one place? If two different records need to point at the same instance, it's an entity, not a copy-pasted value.
  4. Does the business ask "how many," "how long," or "who changed it" about this concept? Those questions require history, which requires rows, not a column.
  5. Would you ever need to query it independently of its parent — "show me all overdue statuses" rather than just "show me this order's status"?
  6. Does it carry more than 2-3 sub-attributes of its own (street, city, postal code, geocode) that would otherwise bloat the parent table?
  7. Is it meaningful to a different job the user is trying to get done? If the concept shows up as a distinct step in a Jobs to Be Done analysis — something the user is hiring the product to track or progress — it usually deserves its own identity in the model, not just a field on something else.

Score two or more "yes" answers and you're almost certainly looking at an entity. One "yes" is usually a value object. Zero is a plain attribute.

When to Resist the Urge to Model Everything as an Entity

Over-modeling is a real cost, not a theoretical one: every extra entity is a join, a migration, an API endpoint, and a UI screen someone eventually has to maintain. The discipline isn't "promote every noun" — it's promoting only the nouns that clear the identity-lifecycle-attributes bar, and deliberately leaving the rest as fields.

A few concrete signs you're over-modeling:

  • You've created an entity with no lifecycle of its own — it's created once and never updated or queried independently (a strong sign it should collapse back into the parent row).
  • Every query that touches the new entity also joins back to its one and only parent, with no case where it's queried alone.
  • The team can't name a single future feature or report that needs the entity's own history.
  • The "entity" has one attribute. Color as a colors table with a single name column is usually just a VARCHAR with a check constraint, not a table.

The inverse trap matters too. Entities don't exist in isolation — they connect into the flows customers actually move through, which is why mapping them against a customer journey is a useful sanity check: if a "status" or "stage" concept in your schema doesn't line up with a stage a real customer experiences, you may be modeling an internal process artifact rather than something the product genuinely needs to expose. And once an entity is real, it usually becomes a resource in your API surface — the same identity-and-lifecycle test is exactly what determines whether something gets its own endpoint in API product design, rather than being embedded as a nested field on another resource's response.

Seeing the Tradeoff Live: How Prodinja Makes Entity Promotion Visible

The entity-versus-attribute decision is usually invisible — it happens silently in a spec or a developer's head, and nobody sees the cost until a migration is painful. Prodinja's Data Modelling tool makes that tradeoff a visible, reversible choice instead of a buried one: you describe your domain nouns, and promoting a concept from a field to its own entity immediately regenerates the DDL, showing the new CREATE TABLE statement and the foreign key it costs you.

That immediacy is the point. When "Status" moves from a column to its own entity in the tool, the generated schema shows the new table appear alongside the foreign key now sitting on the parent — the exact cost this article has been describing, rendered as SQL instead of left as an abstract argument. It doesn't run your production migrations or replace the judgment in the checklist above; it's designed to let you see the consequence of the decision before you've written a line of application code, which is the moment this decision is cheapest to get right.

Key Takeaways

  • An entity needs all three: independent identity, its own lifecycle, and attributes that matter on their own — not just one of the three.
  • Address is often a value object embedded in its parent, and only becomes a full entity when it's reused across records or needs history.
  • Status is the classic trap: because it has a real lifecycle (transitions, timestamps, actors), it belongs in its own table or history record, not a free-text column.
  • Ralph Kimball's Slowly Changing Dimension (SCD Type 2) pattern and Eric Evans' Entity/Value Object distinction from Domain-Driven Design both formalize this exact judgment call.
  • Use the checklist — identity, lifecycle, reuse, "how long/who," independent queryability, sub-attribute count, and job relevance — before adding any new table.
  • Over-modeling has a real cost: every unnecessary entity is a join and a migration someone else maintains later.
  • Tools that regenerate DDL as you promote a concept, like Prodinja's Data Modelling tool, make the entity-versus-attribute cost visible instead of theoretical.

Frequently Asked Questions

What is the difference between an entity and an attribute in data modeling?

An entity is a noun with its own identity, lifecycle, and independent attributes — it gets a table and a primary key. An attribute is a property that only describes another thing and has no meaning or identity on its own — it gets a column. The test is whether the concept could be tracked, queried, or referenced independently of its parent.

How do you know if something should be a separate table?

Run the checklist: does it need its own ID, does it change on its own schedule, is it referenced from more than one place, and does the business ask "how long" or "who changed it" about it? Two or more "yes" answers point to a separate table; zero or one usually means it's fine as a column.

Is Status always its own entity?

Not always as a full standalone entity, but it almost always needs a history mechanism rather than a plain overwritable column, because status changes over time and stakeholders eventually ask about duration, transitions, or ownership. At minimum, pair the status column with a status_history table; for complex workflows, model status transitions as their own entity.

What's the difference between an entity and a value object?

A value object (from Eric Evans' Domain-Driven Design) is defined entirely by its attribute values and is interchangeable — two instances with identical fields are the same thing. An entity has identity that persists independently of its attribute values — two instances with identical fields can still be different things, like two orders with the same total.

Do I need a separate Address table for every product?

No. If an address is captured once, never reused, and never needs historical tracking, a single text field or embedded JSON value object is simpler and correct. Promote it to its own entity only when the address is shared across multiple records, needs validation or geocoding, or must retain history after it changes.