Data modeling is a process of mapping entities, attributes, and relationships before deciding how a database will store them. The entities are the easy part — every product knows its users, orders, and posts. The craft is in the relationships: which records point at which, how many, and where that edge physically lives. Get the edges right and queries write themselves; get them wrong and every feature fights the schema.
Key takeaways
| Question | Answer |
|---|---|
| The three levels | Conceptual (what) → logical (structure) → physical (engine) |
| The three edges | One-to-one · one-to-many · many-to-many |
| Relational tools | Foreign keys; junction tables for many-to-many |
| Document tools | Embedding, Pointers (references), Relations/ID arrays |
| The modern rule | Model for access patterns — data read together lives together |
The same relationships, both worlds
Relational first — the foreign key carries one-to-many, and many-to-many must decompose through a junction:
-- 1:N — the foreign key lives on the "many" side
CREATE TABLE books (
id bigint PRIMARY KEY,
title text NOT NULL,
author_id bigint NOT NULL REFERENCES authors(id) ON DELETE CASCADE
);
-- M:N — no single FK can express it; a junction table holds both keys
CREATE TABLE book_genres (
book_id bigint REFERENCES books(id),
genre_id bigint REFERENCES genres(id),
added_at timestamptz DEFAULT now(), -- junctions can carry attributes
PRIMARY KEY (book_id, genre_id) -- composite key: one edge, once
);
Document world, same model: the one-to-many edge is a typed Pointer, the many-to-many edge is a Relation — no junction table to invent, and the edge is declared in application code:
// JavaScript / Node.js — Back4app JS SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
const author = await new Parse.Query('Author').get(authorId);
const book = new Parse.Object('Book');
book.set('title', 'Dune');
book.set('author', author); // Pointer — typed one-to-many edge
await book.save();
const genres = book.relation('genres'); // Relation — many-to-many, unbounded
genres.add([sciFi, classics]);
await book.save(); // Flutter / Dart — Back4app Flutter SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
final book = ParseObject('Book')
..set('title', 'Dune')
..set('author', author.toPointer()); // Pointer — typed one-to-many edge
await book.save();
book.addRelation('genres', [sciFi, classics]); // Relation — many-to-many
await book.save(); // iOS / Swift — Back4app Swift SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
var book = Book()
book.title = "Dune"
book.author = try author.toPointer() // Pointer — typed one-to-many edge
let saved = try await book.save()
let relation = try saved.relation("genres")
try await relation.add([sciFi, classics]).save() // Relation — many-to-many // Android / Kotlin — Back4app Android SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
val book = ParseObject("Book").apply {
put("title", "Dune")
put("author", author) // Pointer — typed one-to-many edge
}
book.save()
val genres = book.getRelation<ParseObject>("genres") // Relation — many-to-many
genres.add(sciFi)
genres.add(classics)
book.save() Conceptual vs. logical vs. physical: the three levels
| Dimension | Conceptual | Logical | Physical |
|---|---|---|---|
| Question answered | What exists, how related | What fields, what keys | How stored, how fast |
| Audience | Everyone | Designers | Engineers + the engine |
| Contains | Entities, relationships | + attributes, types, cardinality | + indexes, constraints, partitions |
| Changes when | The business changes | Requirements refine | The engine or scale changes |
The discipline the levels enforce: don’t argue about indexes while the team still disagrees about what an “order” is.
Every relationship, every implementation
The table the SERP never built — each edge type with its implementation in both worlds:
| Relationship | Example | Relational implementation | Document implementation |
|---|---|---|---|
| One-to-one | User ↔ profile | FK with UNIQUE, or same row | Embed it — almost always |
| One-to-few | Person → addresses | Child table + FK | Embedded array (bounded) |
| One-to-many | Author → books | FK on the many side | Pointer on the many side |
| One-to-enormous | Device → events | FK + partitioning | Pointer on the child; never an array |
| Many-to-many | Books ↔ genres | Junction table, composite PK | Relation or arrays of pointers |
| Self-referencing | Employee → manager | FK to own table | Pointer to own class |
Cardinality notation, for reading diagrams: crow’s-foot draws a bar for “one” and a three-pronged fork for “many”; the ER tradition writes 1 and N on the lines; UML writes multiplicities like 1 and *. Three dialects, one grammar.
Normalize, denormalize, or embed?
Relational modeling starts from normalization — split data so every fact lives once, protecting writes from anomalies. Analytical and document modeling deliberately bend it: denormalization duplicates read-hot fields to kill joins, and embedding is denormalization’s document-native cousin. The embed-vs-reference checklist compresses to three questions: read together? owned by one parent? bounded in size? Three yeses embed; any no references. And over all of it, the modern rule that generalist guides skip: list the queries first. A model is correct when the access patterns it must serve are cheap — entities alone can’t tell you that.
Common use cases
- Designing a new backend. The classic pass: name entities, draw edges, choose implementations per the table above — before the first line of code.
- The junction-with-attributes pattern. Enrollments, memberships, line items — when the edge itself has data (date, quantity, role), the junction (or an edge class with two pointers) is a first-class entity.
- Untangling a grown schema. Symptoms map to edges: duplicated rows reveal a mis-modeled many-to-many; document bloat reveals an unbounded embed.
- Migrating between worlds. Relational→document means re-deciding every FK as embed vs. pointer; the table above is the translation dictionary.
- AI and analytics feeds. Warehouses want the edges explicit and stable — modeling debt surfaces the day you try to export.
How should you model each edge? A decision matrix
| Choose… | When… | Watch out for… |
|---|---|---|
| Embed | Read together, owned, bounded | Growth: today’s “few” becomes tomorrow’s thousands |
| Pointer / FK | Independent lifecycle, high cardinality | Index it — every lookup and include pays |
| Relation / junction | Many-to-many, or the edge carries data | Query direction: know which side you’ll ask from |
| Duplicate (denormalize) | A read-hot field crossing an edge | Update fan-out — duplication is a debt with interest |
Limitations and trade-offs
- Models freeze assumptions. Cardinality decisions (“a user has one address”) become schema; cheap to change on paper, expensive after launch — model for tomorrow’s cardinality.
- Both worlds punish the unindexed edge. FK columns and pointer fields are join paths; forgetting their indexes is the most common silent performance bug.
- Embedding trades integrity for locality. No engine enforces that an embedded copy stays consistent with its source — that’s your update logic now.
- Junctions multiply joins; relations hide them. Every many-to-many read crosses the edge store — budget the query path, whichever world you’re in.
- Access-pattern modeling has a cost too. Optimizing for today’s queries can wed the schema to today’s product; keep the conceptual model around as the neutral ground truth.
Data modeling on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Its modeling vocabulary is the document column of the tables above, made first-class: Pointers are typed one-to-many edges, Relations carry many-to-many without a hand-built junction, and arrays cover the bounded-few. Every edge you declare is immediately traversable — include() walks pointers in one request, GraphQL nests relations in one query — and the schema stays visible and editable in the dashboard, so the physical model never drifts out of sight of the team that designed the conceptual one.
Frequently asked questions
What is data modeling?
The process of mapping an application's information — its entities, their attributes, and the relationships between them — into a design a database can store. It typically moves through three levels of detail: a conceptual model naming the entities, a logical model adding attributes and keys, and a physical model committing to tables, columns, and indexes in a specific engine.
What are the three types of data models?
Conceptual, logical, and physical — the same design at increasing zoom. Conceptual answers "what exists and how does it relate" for business stakeholders. Logical adds attributes, types, and keys while staying technology-agnostic. Physical commits to an actual engine: tables or collections, indexes, constraints. Each level is the previous one plus decisions.
What is a one-to-many relationship?
One parent record relating to many children, each child belonging to exactly one parent — a customer and their orders, an author and their books. It is the most common relationship in any schema. Relational databases implement it with a foreign key on the many side; document databases use an embedded array for small bounded sets or a pointer to the parent for everything else.
What is a many-to-many relationship and why does it need a junction table?
Both sides relate to many of the other — students and courses, books and genres. A single foreign-key column can only point at one row, so relational databases decompose many-to-many into two one-to-many relationships through a junction table holding both keys (and often relationship attributes like an enrollment date). Document databases skip the junction: arrays of references, or a dedicated relation type, carry the edge directly.
How do I tell one-to-many from many-to-many?
Ask the ownership question in both directions. "Can one author have many books?" Yes. "Can one book have many authors?" If no — one-to-many, foreign key on the book. If yes — many-to-many, junction table or relation. Getting this wrong is the classic modeling mistake: a many-to-many modeled as one-to-many either duplicates rows or silently loses edges.
What is cardinality in an ER diagram?
How many instances of one entity can relate to instances of another — one-to-one, one-to-many, many-to-many. Diagrams express it in one of three notations: numbers and letters on the connecting lines, crow's-foot symbols (a bar for one, a three-pronged fork for many), or multiplicity ranges like 1 and asterisk. Same semantics, different drawing conventions.
How do document databases model relationships without foreign keys?
With three tools: embedding (nest the related data inside the parent document — right when it is read together, owned, and bounded), pointers or references (store the related document's ID as a typed field — right for independent lifecycles and high cardinality), and relation objects or ID arrays for many-to-many. The guiding rule flips from normalization to access patterns: data read together should live together.
What are the most common data modeling mistakes?
A stable top five: modeling many-to-many as one-to-many; forgetting indexes on foreign-key and pointer fields (every join and lookup pays); unbounded embedded arrays in document schemas; premature denormalization before any query proved slow; and modeling from the entities alone without listing the queries — the access patterns — the model must serve.