Relational modeling challenge
"Make invalid states unrepresentable" and the relational model
A holy grail of data modeling is “making illegal states unrepresentable”. In programming languages theory and practice we want to represent complex data in such a way that it’s impossible to use the data incorrectly. Recently, modern programming languages such as Rust have popularized this approach.
I’d like to present the following challenge: build a relational data model that implements a real-world problem in a way that it’s impossible to store contradictory data.
First, a real-world problem:
Suppose that we want to build an app for tracking habits that we want to avoid, for example takeaway coffee consumption. Every day the user of this app needs to record each time they had takeaway coffee: time of the day, type of coffee, price. If the user managed to avoid coffee all day, they can confirm that by clicking the “coffee-free day” button.
We want to maintain awareness, so we require that the user communicates with the app every day. If the user did not use the app the entire day, we need to distinguish that.
Summarizing, here are three possibilities for each day:
user had takeaway coffee, or several: here is the list;
user had a coffee-free day;
user avoided our app.
Conditions of the challenge
You need to submit a relational database schema that implements the problem described above. This schema must prevent incorrect combinations of data in different tables (see below for examples). The most convenient way would probably be something like https://www.db-fiddle.com/.
The goal is to learn something about relational modeling. It’s possible that somebody, maybe you, would instantly present a solution and it will be obvious in retrospect. That would be the best outcome for me personally, because I have no idea how to solve it. Moreover, I conjecture that it’s impossible within the classic relational model framework. But I may be wrong.
You need to use a classic relational model, not violating the first normal form. You can use standard database constraints.
Using JSON-typed and similar column data types does not count, because it’s trivial to implement the solution in JSON (left as an exercise for the reader).
Using array-typed columns is dubious: it’s okay if arrays contain simple types such as numbers or strings, but not okay if they contain user-defined record types, or something like that. Basically, you can’t smuggle in JSON, because it would violate the first normal form requirement. If you think that arrays also violate 1NF you’re welcome not to use them too.
You can’t use database triggers. Implementing this with triggers is tedious but straightforward (left as an exercise for the reader).
Your solution should work in more or less any reasonable DBMS. It’s okay to submit solutions that rely on specific features of some DBMS, if those features are not JSON or triggers in disguise.
Acceptable (and welcome) solution is a proof that this is in fact impossible, and an explanation why.
It’s probably possible to somehow work around those requirements on some technicality, but this may or may not be an interesting solution. This challenge is not a trick question, I’m looking for algebraically sound responses here, that could be taught to people.
I’m eagerly waiting for what the community has to say on this.
P.S.: textbook schema is non-compliant
Here is an explanation of why the textbook schema is not compliant. This is trivially modelled like this:
CREATE TABLE coffee_free_days (
day DATE NOT NULL PRIMARY KEY
);
CREATE TABLE coffees (
id INTEGER NOT NULL PRIMARY KEY,
day DATE NOT NULL,
daytime VARCHAR(5) NOT NULL,
what VARCHAR(32) NOT NULL,
price DECIMAL(5, 2) NOT NULL
);
If there is a row in the “coffee_free_days” table, that means that the user had a coffee-free day. In that case, there must be no rows in the “coffees” table”.
If the user had some coffees, we insert rows into the “coffees” table. In that case, there must be no row in the “coffee_free_days” table.
In this textbook implementation it’s possible to create contradictory data:
insert a row into “
coffee_free_days” with the value (“2025-02-01”);insert a row into “
coffees” with values (1234, “2025-02-01”, “13:38”, “Spanish latte”, 8.50);
There is nothing in this schema that prevents this incorrect combination of rows.
The challenge is to prevent any possible contradiction, using features of the relational model as implemented by commonly available systems.
I’m interested to hear what you have to say on this.

Hello! I hope it’s not too late to chime in — apologies for the necroposting, I understand that this challenge is already almost a year old, but I couldn’t find any published results or a summary of submitted solutions. If there were any conclusions, I’d really appreciate a pointer.
In the proposed solution I use a custom type (without collections), which you consider acceptable for 1NF (if I understood Making sense of 1NF, pt. II correctly). It can also be replaced with plain columns if needed — we would lose some elegance, but not reliability.
Based on that interpretation, I believe there is a clean relational solution that:
* enforces all invariants,
* stays within 1NF as you describe it,
* avoids JSON, arrays of records, and triggers,
* and prevents all contradictory states.
Key ideas
A “coffee day” must contain at least one coffee entry.
Since enforcing that via a child table would require triggers, we instead store the first coffee directly in the parent table as a composite (scalar) coffee value.
Additional coffees (zero or more) may be stored in a child table.
This ensures that a “coffee day with an empty list” is structurally impossible.
And to prevent coffee records from appearing for non-coffee days, we include the day type in the parent table’s unique key, that is used as fk in coffee-entries table and add a check constraint type = 'coffee' in the table.
To prevent multiple records with different types for the same day, we also add a UNIQUE constraint on the day column in the parent table.
Why this satisfies the challenge
* Exactly one of the three states per day. day is unique; type is functionally dependent.
* No contradictory combinations across tables. Foreign keys + CHECK constraints ensure state coherence.
* A coffee day must contain ≥1 coffee. Enforced by the composite-typed coffee column being non-
* Coffee-free / avoid days cannot have coffees. Both the parent and child tables forbid this.
* Composite types are 1NF-safe - Per “pt. II”, they are fixed-arity atomic values, not nested relations.
* No JSON, no arrays-of-records, no triggers
Here is db-fiddle with propoed schema: https://www.db-fiddle.com/f/6bksKju2M8UJYJ4m96NoRu/0
A couple of thoughts, overlapping other comments somewhat:
1. CROSS-TABLE CONSTRAINTS NOT WIDELY SUPPORTED
You want an entry in either 'coffee_free_days' or 'coffees' for a given day, but not both. Enforcing that as part of the schema definition requires a CONSTRAINT across both tables. Alas, the only cross-table CONSTRAINT generally-supported is a FOREIGN KEY, which can't do what you're seeking.
2. TRYING TO IMPLEMENT A SUBTYPE
In this case, you have an entity of 'day', and you want 3 specializations of it:
- implicitly no coffee drunk: nothing recorded for the day.
- explicitly no coffee drunk: "coffee-free day" button clicked.
- one or more 'coffees' recorded for the day.
Relational model doesn't support the concept of subtype. We might get something close in this case with:
CREATE TABLE days(
day DATE NOT NULL PRIMARY KEY ) ;
CREATE TABLE coffees(
id INTEGER NOT NULL PRIMARY KEY,
day DATE NOT NULL REFERENCES days( day ),
time TIME NOT NULL,
what VARCHAR( 32 ) NOT NULL,
price DECIMAL( 5,2 ) NOT NULL ) ;
This changes a couple of semantics:
- 'days' records days when either "coffee-free day" was pressed *or* the first linked "coffees" entry for the day was entered, and
- a 'days' entry *without* linked 'coffees' must have been recorded as a "coffee-free day".
This would required the code to check for the following errors:
- "coffee-free day" is pressed when a "days" entry already exists linked to a "coffees" entry
- a coffee is entered but a "days" entry already exists *without* a linked "coffees" entry implying it was previously recorded as a "coffee-free day".
Actually, I have many more thoughts, but that would be a much longer conversation.