DataGENERALTEAM-SPECIFICILLUSTRATIVE

Data Modelling From Plain English

"Users can create projects and invite members" hides User, Project, Membership and Invitation — two of which are verbs. The move is to read requirements as data until a first schema falls out, then hand the schema to Database Engineering to make it correct.

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

How do I get from a sentence a non-engineer wrote to a first schema I could actually create, without designing a database I do not yet understand?

The situation

The requirement says "users can create projects and invite members, and members can leave". That is the whole spec. I need tables by tomorrow and I do not want to invent them and find out in a month that "invite" was the hard part.

The reflex

Create users and projects with a members array column on the project, or a project_id on the user, and move on. It matches the sentence word for word and takes five minutes.

Why it stalls

The sentence had four entities and the schema has two. "Invite" is a verb with a lifecycle — sent, accepted, declined, expired — and "member" is a relationship with facts of its own — role, joined at, left at. Both were flattened into a column, and the first requirement about a pending invitation has nowhere to live.

What the reflex produces — and fails to produce
  • The sentence had four entities and the schema has two. "Invite" is a verb with a lifecycle — sent, accepted, declined, expired — and "member" is a relationship with facts of its own — role, joined at, left at. Both were flattened into a column, and the first requirement about a pending invitation has nowhere to live.
  • A project_id on the user means a user is in one project. A members array on the project means "who invited whom, and when" is gone. Either shape was chosen by the sentence's grammar, not by the questions a real user will ask.
  • The schema is created, then populated, then depended on, and the day "can we see who left and when?" arrives, the answer is a migration with data that no longer exists.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Read the sentence three times: for nouns (entities), for verbs (relationships, and the entities hiding in them), and for lifecycle words — invite, accept, leave, cancel, expire. A verb with a lifecycle is an entity: it has states, times and an actor, and someone will ask about them (Finding the State Machine).
  • For each relationship, write the concrete question a user or admin will ask about it. "Who is in this project?" is answered by a Membership. "Who was invited but has not joined?" is answered only by an Invitation. "When did they leave?" needs left_at on Membership, or a row that is never deleted. The questions decide the shape; the grammar does not.
  • Write the first schema as the most direct expression of the entities and questions — a table per entity, a foreign key per many-to-one, a row per fact — and stop. Do not optimise, denormalise or index; that is what the schema is handed to Database Engineering for, and doing it now means doing it without the queries that would justify it (From Requirements to Tables).
  • Walk the workflow through the schema on paper before creating it: create a project, invite someone, they accept, they leave, they are re-invited. Every step must be a row inserted or updated, and if a step has no row, an entity is missing.

The sentence, and what is still unknown

The board is the requirement after the three readings. The known column is what the sentence settled; the unknowns are the facts about the data that would change the draft and that the sentence does not decide. Each becomes a question the founder can answer with a concrete case.

"Users can create projects and invite members, and members can leave"
known
  • Entities: User, Project, Membership (with role and dates), Invitation (with a status).
  • Project has one creator; Membership is many-to-many between User and Project.
  • "Leave" is a state change, so Membership rows are kept, not deleted.
assumed
  • ~One role per membership. A user who is both admin and viewer of a project would break this — to be asked.
unknown → question → experiment
  1. ? What is an invite, really?

    becomes Can a person be invited who has no account yet — so Invitation refers to an email, not a User — and what happens when they sign up with a different email?

    experiment Walk "invite alice@example, Alice signs up as alice.b@example" through the draft on paper; see which row cannot be created.

  2. ? Leaving.

    becomes When the last admin leaves a project, what happens — is it forbidden, does the project become orphaned, or does someone inherit?

    experiment Ask the founder with that exact case; the answer is an invariant to enforce, not a schema change.

  3. ? Re-inviting.

    becomes Can someone who left be invited again, and is their old membership history visible to them and to admins?

    experiment Write the "current members" and "membership history" queries against the draft; if the unique constraint blocks the second membership, it is on the wrong columns.

The first schema, and what it deliberately lacks

The draft is the most direct expression of the entities and the questions. Read it for what is absent — no indexes beyond keys, no counts, no copied names — and for the two decisions that came from questions rather than from the sentence: left_at, and the partial unique constraint that allows re-joining.

First draft — before Database Engineering
1create table users (id serial primary key, email text unique not null, name text);
2create table projects (id serial primary key, name text not null,
3 created_by int not null references users(id));
4
5-- "members" was a verb with facts: role, when they joined, when they left
6create table memberships (id serial primary key,
7 project_id int not null references projects(id),
8 user_id int not null references users(id),
9 role text not null,
10 joined_at timestamptz not null default now(),
11 left_at timestamptz); -- null = current
12create unique index one_current_membership
13 on memberships (project_id, user_id) where left_at is null; -- history allowed
14
15-- "invite" was a verb with a lifecycle
16create table invitations (id serial primary key,
17 project_id int not null references projects(id),
18 invited_by int not null references users(id),
19 email text not null, -- may not be a user yet
20 status text not null default 'pending',
21 sent_at timestamptz not null default now(),
22 responded_at timestamptz);

Two of the four tables came from verbs. email on invitations, not user_id, came from an unknown that became a question. Nothing here is indexed for a query, because no query exists yet; that is the handover.

What each workflow step does to the draft

The walk-through is the test of the draft: every step of the workflow must be a row, and the table shows which. A step with an empty cell is a missing entity or a missing column — found on paper, before the migration.

Workflow stepRow inserted / updatedQuestion it later answers
User creates a projectprojects (created_by); memberships (role admin)"Who owns this?" "Who can manage it?"
Admin invites bob@exampleinvitations (pending, email)"Who is invited but not yet in?"
Bob signs up and acceptsinvitations → accepted; memberships (bob, member)"Who is in the project?" "Who invited Bob?"
Bob leavesmemberships.left_at set"Who used to be in this project, and until when?"
Bob is re-invited and acceptsnew invitation; new membership row"Has Bob been here before?" — history, not overwrite
Last admin tries to leave(no row — an invariant refuses it)Not a data question; a rule found by the walk-through

How to do it

Most important first.

  • Underline nouns, circle verbs, box lifecycle words. Every boxed word is an entity candidate with a status.
  • For each relationship, write the questions people will ask of it, in their words. Include the ones about the past ("who used to be…").
  • Draft one table per entity with only the columns the questions need. Foreign keys for many-to-one; a table for many-to-many, named for what it is (Relationships, Keys and Constraints).
  • Walk every workflow step through the draft as an insert or update. A step that changes nothing in the database is a step you have not modelled.
  • Hand the draft over — to your own reading of Database Engineering, or to a teammate — for keys, constraints, normal forms and indexes. Keep the questions list with it; the indexes come from the questions.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • "Users can create projects and invite members, and members can leave." Nouns: User, Project. Verbs: create (User → Project, many-to-one owner), invite (lifecycle: pending, accepted, declined, expired → Invitation entity), be a member (User ↔ Project many-to-many with role and dates → Membership entity), leave (a state change on Membership, so left_at rather than a delete). Four entities from a sentence with two nouns.
  • Questions that shaped it: "who is in project X?" → Membership where left_at is null. "Who has an outstanding invite?" → Invitation where status is pending. "Can someone who left be re-invited?" → a new Invitation and, on accept, a new Membership row, so history is kept and the unique constraint is on (project, user) *where left_at is null*, not on the pair alone.
  • The same move on the store's "customers can return items within thirty days": Return is a lifecycle verb (requested, approved, received, refunded), refers to an OrderItem not an Order (you return a thing, not a purchase), and the question "how many of this item were returned?" means Return carries a quantity. None of that was in the sentence; all of it was in the questions.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • The schema has more tables than the sentence had nouns, and you can say which verb each extra table came from.
  • Every question on the questions list is answerable by a query against the draft, including the ones about the past.
  • Every workflow step corresponds to an insert or update you can name.
  • The draft has no indexes, no denormalised columns and no caching table — those are waiting for queries to justify them.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?Which verbs in this requirement have a lifecycle — states, times, an actor — and are therefore entities?
  • ?What questions will people ask about each relationship, including questions about the past?
  • ?Does every step of the workflow change a row in this draft, and which step does not?
  • ?What have I optimised in this draft before any query asked for it?

What can go wrong

How the move itself fails
  • Every verb becomes an entity. "Users can view projects" does not need a View table unless someone asks who viewed what; the lifecycle test is whether there are states and facts, not whether there is a verb.
  • History is kept for everything. left_at on Membership is cheap; a full audit trail on every table before anyone has asked for one is a schema twice the size, and it is Database Engineering's decision to make with the queries in hand.
  • The draft is optimised as it is written — a member_count on Project, a denormalised project name on Membership — because it seemed obvious. Each is a second source of truth introduced before the first has any load (Source of Truth).
What the move costs
  • Four tables instead of two is more code and more joins, and if invitations never become a feature, it was a table for nothing.
  • Keeping history (left_at rather than delete) makes every "current members" query carry a filter, forever.
  • Stopping before optimisation means the first schema will be changed by the person who does optimise it — and it should be.
Misreads
  • "This is the schema." It is the first schema. Keys, constraints, normal forms and indexes are the next domain's work, and the draft is the input to it, not the output.
  • "Lifecycle words always mean a table." They mean a state with facts. "Members can leave" became a column and a rule; "invite" became a table. Both were lifecycle words.
  • "Plain English is too vague to model from." It is exactly precise enough to ask questions of, and the questions are what the model is made from.

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALNouns, verbs, lifecycle words and the questions list produce a first draft in any domain; the draft is relational here because the store and the project tool are, but the same reading precedes a document or graph model.
  • TEAM-SPECIFICA solo builder does both halves — discovery and schema engineering — and the risk is doing them at once. On a team the handover is real: the questions list travels with the draft, because whoever indexes it needs to know what will be asked.
  • ILLUSTRATIVEThe project tool sentence, the thirty-day return window and the store are invented to show the shape of the move.

Where the depth lives

This domain asks the question and hands the answer off by name.