{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"the-data-model-before-the-screens","title":"The data model comes before the screens","url":"https://addaly.com/learn/building-apps-with-ai/the-data-model-before-the-screens","position":11,"of":87,"minutes":9,"answer":"Settle the nouns, their fields and how they relate before any screen exists, because a wrong data model outlives every other mistake in the project.","keyIdea":"Settle the nouns, their fields and how they relate before any screen exists, because a wrong data model outlives every other mistake in the project."},"body":{"format":"markdown","text":"## Screens are cheap, data is not\n\nA screen can be redrawn in ten minutes. A data model with a mistake in it is still with you in six months, because by then there are two thousand rows shaped by that mistake, code in nine files that assumes it, and a migration nobody wants to run.\n\nSo before the first screen, spend fifteen minutes on the nouns.\n\n## Write the nouns and what each one holds\n\nFor a salon booking app:\n\n```\ncustomer   id, name, phone, created_at\nstylist    id, name, works_monday..works_sunday\nservice    id, name, duration_minutes, price_paise\nbooking    id, customer_id, stylist_id, service_id,\n           starts_at, status, created_at\n```\n\nFour nouns, about twenty fields, and you have just made every decision that matters. Notice what the writing forced you to settle:\n\n**A booking points at a service, so its duration comes from the service.** Which means changing a haircut from 30 to 45 minutes changes every future booking, and possibly every past one. Is that what you want? You now have to answer, which is the point.\n\n**Price is `price_paise`, an integer.** Money in floating-point numbers produces 0.1 + 0.2 = 0.30000000000000004, and eventually an invoice that is one paisa wrong and cannot be explained. Store the smallest unit as an integer, always. Rupees, cents, kobo, centavos.\n\n**`status` exists**, so a cancelled booking is a booking with a status rather than a deleted row. Deleted rows cannot be counted, audited or restored.\n\n## The four relationship shapes\n\nAlmost everything is one of these, and saying which is most of the design:\n\n**One to many.** One customer has many bookings. The many side holds the link: `booking.customer_id`. This is the common case and models get it right.\n\n**Many to many.** A stylist can perform many services and a service can be performed by many stylists. This needs a third table: `stylist_service` with two ids and nothing else. Models often skip this and stuff a comma-separated list into a text column, which works until you need to search it.\n\n**One to one.** Rare, and usually a sign that two tables should be one.\n\n**Belongs to nothing.** A settings row, a price list. Fine, but ask whether it should belong to an account, because the day you have two salons the answer becomes yes and the migration is painful.\n\n## Three questions that prevent most redesigns\n\n**What happens when this is deleted?** If a customer is deleted, do their bookings go? Stay as orphans? Keep the name as a snapshot? There is no default right answer, and the database will pick one for you if you do not.\n\n**What has to be true across two rows?** \"Two bookings cannot overlap for the same stylist\" is not a property of one booking. It cannot be enforced by a field. It needs a check when writing, and ideally a constraint in the database, and it is exactly the rule a model will implement in the browser where it does nothing.\n\n**What do you need to know later that you are not storing?** `created_at` on everything, always. The day somebody asks how many bookings you took in July, a missing timestamp means the answer does not exist and cannot be reconstructed.\n\n## Hand it over as text\n\nThe block of nouns above, pasted into your tool, is worth more than four paragraphs of description. It removes the guesswork about field names, which is what makes a model write `customerName` in one file and `customer_name` in the next.\n\n> Figure: Which shape is this relationship. A two by two grid, rows One customer and Many customers, columns One booking and Many bookings: One customer and One booking: One to one; One customer and Many bookings: One to many; Many customers and One booking: Many to one; Many customers and Many bookings: Many to many. Saying which one you have is most of the design. Many-to-many is the one a model skips, stuffing a comma-separated list into a text column instead, which works until you need to search it.\n\nOne useful instruction to add: *generate the schema from this and stop. Do not build any screens yet.* Then read what came back and check the column types, because that is where the quiet decisions hide: a phone number stored as a number loses its leading zero, and a date stored as text cannot be sorted correctly.\n\n## Draw it if it helps\n\nFour boxes and some arrows on paper is a perfectly good entity diagram. If you want it on a screen, **draw.io** and **Excalidraw** are free and need no account, and **dbdiagram.io** turns a text description into a diagram directly. None of this is required. The nouns list does the work; the picture only makes it easier to argue about.\n\n## The limitation\n\nYou will get this wrong the first time. Everybody does: the thing you learn in week three changes what week one should have been. That is not a reason to spend a month planning. It is a reason to know how to change a schema safely, which is a whole lesson later in this course, and to make the first version small enough that changing it is cheap."},"check":{"question":"A model stores a service's price in a column of type `decimal` and a stylist's list of services as a comma-separated text field. Which is the more serious problem, and why?","options":[{"text":"The price column, because decimal cannot represent currency accurately.","correct":false,"tempting":"Confuses decimal with binary floating point; decimal is a reasonable money type, and the real hazard is float."},{"text":"The comma-separated list, because a many-to-many relationship cannot be searched, joined or constrained once it is flattened into text.","correct":true},{"text":"Neither, since both can be corrected later with a migration.","correct":false,"tempting":"True in principle and misleading in practice; the text list has by then been read and written by code in several places, which is what makes the migration expensive."},{"text":"The price column, because prices should always be integers of the smallest unit.","correct":false,"tempting":"Names a genuine convention but ranks it above a structural error; an integer price is safer, while flattened relationships break queries outright."}]},"prev":{"slug":"describing-what-you-want","title":"Describing what you want","url":"https://addaly.com/learn/building-apps-with-ai/describing-what-you-want"},"next":{"slug":"slicing-a-feature-into-checkable-steps","title":"Slicing a feature so every step is checkable","url":"https://addaly.com/learn/building-apps-with-ai/slicing-a-feature-into-checkable-steps"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The data model comes before the screens', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/the-data-model-before-the-screens (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The data model comes before the screens","courseTitle":"Building Apps with AI","module":{"index":2,"slug":"saying-what-you-want","title":"Saying what you want, precisely enough"},"answer":null,"keyIdea":"Settle the nouns, their fields and how they relate before any screen exists, because a wrong data model outlives every other mistake in the project.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"cols":["One booking","Many bookings"],"rows":["One customer","Many customers"],"type":"matrix","cells":[["One to one","One to many"],["Many to one","Many to many"]],"title":"Which shape is this relationship","caption":"Saying which one you have is most of the design. Many-to-many is the one a model skips, stuffing a comma-separated list into a text column instead, which works until you need to search it.","cellLabels":[["usually one table","link on the many side"],["the same, reversed","needs a join table"]]},"alt":"Figure: Which shape is this relationship. A two by two grid, rows One customer and Many customers, columns One booking and Many bookings: One customer and One booking: One to one; One customer and Many bookings: One to many; Many customers and One booking: Many to one; Many customers and Many bookings: Many to many. Saying which one you have is most of the design. Many-to-many is the one a model skips, stuffing a comma-separated list into a text column instead, which works until you need to search it."}},"blocks":[{"t":"h2","id":"screens-are-cheap-data-is-not","runs":[{"text":"Screens are cheap, data is not"}]},{"t":"p","runs":[{"text":"A screen can be redrawn in ten minutes. A data model with a mistake in it is still with you in six months, because by then there are two thousand rows shaped by that mistake, code in nine files that assumes it, and a migration nobody wants to run."}]},{"t":"p","runs":[{"text":"So before the first screen, spend fifteen minutes on the nouns."}]},{"t":"h2","id":"write-the-nouns-and-what-each-one-holds","runs":[{"text":"Write the nouns and what each one holds"}]},{"t":"p","runs":[{"text":"For a salon booking app:"}]},{"t":"code","lang":"","label":"Text","text":"customer   id, name, phone, created_at\nstylist    id, name, works_monday..works_sunday\nservice    id, name, duration_minutes, price_paise\nbooking    id, customer_id, stylist_id, service_id,\n           starts_at, status, created_at"},{"t":"p","runs":[{"text":"Four nouns, about twenty fields, and you have just made every decision that matters. Notice what the writing forced you to settle:"}]},{"t":"p","runs":[{"text":"A booking points at a service, so its duration comes from the service.","b":true},{"text":" Which means changing a haircut from 30 to 45 minutes changes every future booking, and possibly every past one. Is that what you want? You now have to answer, which is the point."}]},{"t":"p","runs":[{"text":"Price is ","b":true},{"text":"price_paise","b":true,"code":true},{"text":", an integer.","b":true},{"text":" Money in floating-point numbers produces 0.1 + 0.2 = 0.30000000000000004, and eventually an invoice that is one paisa wrong and cannot be explained. Store the smallest unit as an integer, always. Rupees, cents, kobo, centavos."}]},{"t":"p","runs":[{"text":"status","b":true,"code":true},{"text":" exists","b":true},{"text":", so a cancelled booking is a booking with a status rather than a deleted row. Deleted rows cannot be counted, audited or restored."}]},{"t":"h2","id":"the-four-relationship-shapes","runs":[{"text":"The four relationship shapes"}]},{"t":"p","runs":[{"text":"Almost everything is one of these, and saying which is most of the design:"}]},{"t":"p","runs":[{"text":"One to many.","b":true},{"text":" One customer has many bookings. The many side holds the link: "},{"text":"booking.customer_id","code":true},{"text":". This is the common case and models get it right."}]},{"t":"p","runs":[{"text":"Many to many.","b":true},{"text":" A stylist can perform many services and a service can be performed by many stylists. This needs a third table: "},{"text":"stylist_service","code":true},{"text":" with two ids and nothing else. Models often skip this and stuff a comma-separated list into a text column, which works until you need to search it."}]},{"t":"p","runs":[{"text":"One to one.","b":true},{"text":" Rare, and usually a sign that two tables should be one."}]},{"t":"p","runs":[{"text":"Belongs to nothing.","b":true},{"text":" A settings row, a price list. Fine, but ask whether it should belong to an account, because the day you have two salons the answer becomes yes and the migration is painful."}]},{"t":"h2","id":"three-questions-that-prevent-most-redesigns","runs":[{"text":"Three questions that prevent most redesigns"}]},{"t":"p","runs":[{"text":"What happens when this is deleted?","b":true},{"text":" If a customer is deleted, do their bookings go? Stay as orphans? Keep the name as a snapshot? There is no default right answer, and the database will pick one for you if you do not."}]},{"t":"p","runs":[{"text":"What has to be true across two rows?","b":true},{"text":" “Two bookings cannot overlap for the same stylist” is not a property of one booking. It cannot be enforced by a field. It needs a check when writing, and ideally a constraint in the database, and it is exactly the rule a model will implement in the browser where it does nothing."}]},{"t":"p","runs":[{"text":"What do you need to know later that you are not storing?","b":true},{"text":" "},{"text":"created_at","code":true},{"text":" on everything, always. The day somebody asks how many bookings you took in July, a missing timestamp means the answer does not exist and cannot be reconstructed."}]},{"t":"h2","id":"hand-it-over-as-text","runs":[{"text":"Hand it over as text"}]},{"t":"p","runs":[{"text":"The block of nouns above, pasted into your tool, is worth more than four paragraphs of description. It removes the guesswork about field names, which is what makes a model write "},{"text":"customerName","code":true},{"text":" in one file and "},{"text":"customer_name","code":true},{"text":" in the next."}]},{"t":"p","runs":[{"text":"One useful instruction to add: "},{"text":"generate the schema from this and stop. Do not build any screens yet.","i":true},{"text":" Then read what came back and check the column types, because that is where the quiet decisions hide: a phone number stored as a number loses its leading zero, and a date stored as text cannot be sorted correctly."}]},{"t":"h2","id":"draw-it-if-it-helps","runs":[{"text":"Draw it if it helps"}]},{"t":"p","runs":[{"text":"Four boxes and some arrows on paper is a perfectly good entity diagram. If you want it on a screen, "},{"text":"draw.io","b":true},{"text":" and "},{"text":"Excalidraw","b":true},{"text":" are free and need no account, and "},{"text":"dbdiagram.io","b":true},{"text":" turns a text description into a diagram directly. None of this is required. The nouns list does the work; the picture only makes it easier to argue about."}]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"You will get this wrong the first time. Everybody does: the thing you learn in week three changes what week one should have been. That is not a reason to spend a month planning. It is a reason to know how to change a schema safely, which is a whole lesson later in this course, and to make the first version small enough that changing it is cheap."}]}],"check":{"question":[{"text":"A model stores a service’s price in a column of type "},{"text":"decimal","code":true},{"text":" and a stylist’s list of services as a comma-separated text field. Which is the more serious problem, and why?"}],"options":[{"text":[{"text":"The price column, because decimal cannot represent currency accurately."}],"correct":false,"tempting":[{"text":"Confuses decimal with binary floating point; decimal is a reasonable money type, and the real hazard is float."}]},{"text":[{"text":"The comma-separated list, because a many-to-many relationship cannot be searched, joined or constrained once it is flattened into text."}],"correct":true,"tempting":null},{"text":[{"text":"Neither, since both can be corrected later with a migration."}],"correct":false,"tempting":[{"text":"True in principle and misleading in practice; the text list has by then been read and written by code in several places, which is what makes the migration expensive."}]},{"text":[{"text":"The price column, because prices should always be integers of the smallest unit."}],"correct":false,"tempting":[{"text":"Names a genuine convention but ranks it above a structural error; an integer price is safer, while flattened relationships break queries outright."}]}],"order":[0,3,1,2],"why":null},"prev":{"slug":"describing-what-you-want","title":"Describing what you want","minutes":8},"next":{"slug":"slicing-a-feature-into-checkable-steps","title":"Slicing a feature so every step is checkable","minutes":8}}}