{"id":"31b4cf1c-28b3-42dc-8e48-a9f3b18c05dd","shortId":"JctxSN","kind":"skill","title":"domain-modeling","tagline":"Use when introducing, reviewing, or renaming a top-level type, table, or domain concept; or choosing where state lives (in-memory vs persistent)","description":"# Domain Modeling\n\n## Overview\n\nThe names and shapes you give the concepts in your code *are* the design. **When you introduce a new domain concept, name it for what it means in the user's world, give it a real type, and decide where its state lives — before you write the methods that act on it.**\n\nThis is a **rigid** skill. Run the decisions in order. If you can't satisfy one, stop and tell the user what's blocking you. When the concept is exposed across a module/package/service boundary, also invoke `api-design` (overlap on type design and value-vs-identity).\n\n## When to invoke\n\nInvoke when you're about to:\n\n- Introduce a new top-level type, class, struct, record, or table that represents a *thing in the domain* (Trader, Portfolio, Booking, Invoice, Reservation)\n- Rename an existing domain concept across files\n- Add a new database table or persistent collection\n- Decide whether a chunk of state should live in memory, in a file, in a key-value store, or in a relational database\n- Replace primitive-typed data (`int`, `string`, `Map<int, Map<int, int>>`) with named domain types\n- Sketch the data model for a new feature, area, or service\n- Evaluate whether an existing domain model is well-structured, or review naming and type choices\n\nIf you're not sure whether a change introduces a *new domain concept* (vs. a local helper), **invoke anyway** — the decisions are cheap, mismodeled domain concepts are not.\n\n### Non-triggers — do NOT invoke for\n\n- Renaming a single local variable inside one function (`x` → `count`)\n- Renaming a private helper function or internal-only struct that doesn't represent a domain concept\n- Adding a new field to an existing type when the field is not itself a new domain concept (a new `lastModified` timestamp on `Booking` — no; a new `CancellationPolicy` type referenced from `Booking` — yes)\n- Adjusting an existing type's representation without changing its meaning (switching `int` user IDs to `long`)\n- Adding a method to an existing domain type (use `clean-code` instead)\n- Defining a DTO that mirrors an existing domain type one-to-one for transport (use `api-design`)\n\n### Language guard\n\nThe typed-domain principles below (`Wlaschin/InvalidStatesUnrepresentable`, `Wlaschin/SmartConstructors`, `Wlaschin/TypesForEffects`, `Fowler/PrimitiveObsession`) fire hardest in languages with sum types and pattern matching (TypeScript, Rust, F#, Haskell, Scala, Kotlin, modern C#). They degrade gracefully in dynamic languages (Python, JavaScript, Ruby) where the agent reaches for frozen dataclasses, `pydantic`, `attrs`, `TypedDict`, or NewType where they help. **Do not be type-system-evangelical** — in a small Python script, a `dict` is the right answer.\n\n## The domain-modeling decisions\n\nRun every decision in order. Do not write the type's methods until decision 5 is settled.\n\n1. **Name the concept the way the domain expert names it.** If the user says \"trader\", \"portfolio\", \"booking\", \"policy\" — that's the type name. Avoid invented programmer terms (`UserDataObject`, `BookingManager`, `PolicyHelper`) when a domain term exists. If the domain expert wouldn't recognize the name, you're inventing a secret vocabulary the next programmer will have to decode. *(North, 97/11.)*\n2. **Make implicit relationships explicit as types or methods.** If the rule is \"some traders cannot view some portfolios,\" prefer `trader.canView(portfolio)` over `portfolioIdsByTraderId.get(...)containsKey(...)`. Replace primitive obsession (raw ints, strings, nested maps standing in for relationships) with named types and operations. *(North, 97/11.)*\n3. **Treat this as design, not typing.** The shape you pick now will outlive most of the code that uses it. Sketch two or three alternatives before committing. Validate the chosen shape against at least two realistic scenarios — if it forces awkward workarounds in either, it's the wrong shape. Code is design; design needs validation. *(Brush, 97/12.)*\n4. **Default to immutable value types and pure transformations.** Unless the concept *intrinsically* has identity and lifetime (a `User`, a `Booking`), prefer value types you construct fresh rather than mutate. Operations that produce new domain objects from old ones are easier to test, reason about, and reuse than methods that secretly mutate shared state. *(Garson, 97/2.)*\n5. **Decide where the state lives — before sketching methods.** Ask: Is this data **large** (won't fit in RAM), **persistent** (survives process restart), or **interconnected** (entities reference each other with consistency rules)? If yes to any two: it belongs in a database (embedded like SQLite is fine for small needs). If no to all three: an in-memory structure is fine. Hand-rolled `Map<int, Map<int, int>>` for what is really a relational dataset is a cost you pay forever. *(Spinellis, 97/48.)*\n6. **Consider whether this concept needs its own little language.** When users keep describing the concept with a constrained vocabulary — rules, validations, query expressions, configuration — and several places in the code re-encode that vocabulary by hand, you are recreating a DSL the slow way. An internal DSL (a fluent API in the host language) lets domain experts read and sometimes write the rules directly. Don't *start* here, but recognize the smell. *(Hunger, 97/23.)*\n7. **Place the concept in one canonical location.** One file, one module, one table — not three competing definitions. If you find a parallel concept already exists under a different name, stop and unify before adding a third. The next programmer should be able to find this concept by searching for the domain term and finding exactly one definition.\n\n## Red Flags\n\nThese thoughts mean STOP — restart the decisions:\n\n| Thought | Reality |\n|---|---|\n| \"I'll use a `Map<int, Map<int, ...>>` — it's just an internal lookup.\" | Nested generic collections standing in for domain relationships are a tacit secret only you understand. Make the relationship a type or a method. (97/11) |\n| \"I'll call it `UserManager` / `DataHelper` / `ServiceUtil` — close enough.\" | If the domain expert wouldn't recognize the name, you've invented a vocabulary the next programmer has to decode. Use the domain term. (97/11) |\n| \"I'll write the methods first and figure out the shape as I go.\" | The shape *is* the design decision. Methods follow. Sketch the shape, validate it against scenarios, then add methods. (97/12) |\n| \"It's just a data class — five mutable fields with getters and setters.\" | Default to immutable value types; reach for mutability only when the concept inherently has identity over time. Mutability is a leading source of defects. (97/2) |\n| \"I'll keep this dataset in a `HashMap` for now — we can move it to a DB later.\" | \"Later\" rarely arrives. If the data is large, persistent, or interconnected, it belongs in a database from the start. SQLite is fine. (97/48) |\n| \"Let me hand-roll consistency between these in-memory collections.\" | Foreign keys, cascading deletes, and unique constraints are what an RDBMS does for free. Hand-rolling them produces dangling-reference bugs. (97/48) |\n| \"I'll add another `BookingDataObject` next to the existing `Booking` — they're slightly different.\" | Two competing definitions of the same domain concept guarantee they will drift. Unify first, then add. (97/11) |\n| \"This rule is just a one-line `if` — no need for a domain method.\" | If the same `if` recurs across the codebase representing the same business rule, it's a method on a domain type. Encapsulate it. (97/11) |\n| \"It's an internal type, naming doesn't matter.\" | Internal today is exposed tomorrow, and the name you pick now will appear in stack traces, logs, and PR diffs for years. Name it for the domain. (97/11, 97/12) |\n| \"I'll use a `string` for the email — we validate it on input.\" | Smart constructor instead. The type itself carries the proof; downstream code receives `EmailAddress`, not `string`, and never re-validates. (`Wlaschin/SmartConstructors`) |\n| \"I'll add a `bannedReason` nullable field that's only set when `status == banned`.\" | Boolean flags and \"valid only in some states\" nullables permit invalid combinations the language won't catch. Use a discriminated union; let the compiler refuse the impossible state. (`Wlaschin/InvalidStatesUnrepresentable`) |\n| \"Both `userId` and `accountId` are `string` — argument order is enough.\" | Mars Climate Orbiter. Brand the types (`UserId`, `AccountId`) so swapped arguments fail at compile time. (`Wlaschin/TypesForEffects`, `Fowler/PrimitiveObsession`) |\n\n## What \"done\" looks like\n\nYou are done when **all** of the following are true:\n\n- [ ] The new concept is named with a term a domain expert would recognize.\n- [ ] Relationships between domain concepts are expressed as types or methods, not as nested primitive collections.\n- [ ] You sketched at least one alternative shape and rejected it for a stated reason.\n- [ ] Mutability is a deliberate choice (this concept has identity) rather than a default (it's just easier).\n- [ ] You decided where the state lives based on size/persistence/interconnectedness — not by reflex.\n- [ ] There is exactly one canonical definition of this concept in the codebase.\n\nIf any box is unchecked, you are not done. Either finish, or revert and re-plan.\n\n## Principles in this skill\n\n| # | Principle | Author |\n|---|---|---|\n| 97/2 | Apply Functional Programming Principles | Edward Garson |\n| 97/11 | Code in the Language of the Domain | Dan North |\n| 97/12 | Code Is Design | Ryan Brush |\n| 97/23 | Domain-Specific Languages | Michael Hunger |\n| 97/48 | Large, Interconnected Data Belongs to a Database | Diomidis Spinellis |\n| `Wlaschin/InvalidStatesUnrepresentable` | Make Invalid States Unrepresentable | Scott Wlaschin |\n| `Wlaschin/SmartConstructors` | Smart Constructors | Scott Wlaschin |\n| `Wlaschin/TypesForEffects` | Types for Effects | Scott Wlaschin |\n| `Fowler/PrimitiveObsession` | Primitive Obsession → Replace Primitive with Object | Martin Fowler |\n\nSee `principles.md` for the long-form distillations, citations, and source links.","tags":["domain","modeling","oribarilan","agent-skills","ai-agents","best-practices","claude-code","claude-code-plugin","claude-code-skills","coding-agents","copilot-cli","copilot-cli-plugin"],"capabilities":["skill","source-oribarilan","skill-domain-modeling","topic-agent-skills","topic-ai-agents","topic-best-practices","topic-claude-code","topic-claude-code-plugin","topic-claude-code-skills","topic-coding-agents","topic-copilot-cli","topic-copilot-cli-plugin","topic-opencode","topic-opencode-plugin","topic-programming-principles"],"categories":["97"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/oribarilan/97/domain-modeling","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add oribarilan/97","source_repo":"https://github.com/oribarilan/97","install_from":"skills.sh"}},"qualityScore":"0.460","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 21 github stars · SKILL.md body (10,077 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:05:32.825Z","embedding":null,"createdAt":"2026-05-08T13:06:23.660Z","updatedAt":"2026-05-18T19:05:32.825Z","lastSeenAt":"2026-05-18T19:05:32.825Z","tsv":"'1':484 '2':544 '3':588 '4':646 '5':481,702 '6':787 '7':863 '97/11':543,587,969,1003,1182,1221,1258,1491 '97/12':645,1036,1259,1501 '97/2':701,1074,1484 '97/23':862,1507 '97/48':786,1115,1151,1514 'abl':905 'accountid':1340,1354 'across':114,170,1203 'act':81 'ad':309,358,897 'add':172,1034,1154,1181,1296 'adjust':342 'agent':431 'alreadi':887 'also':118 'altern':613,1411 'anoth':1155 'answer':461 'anyway':265 'api':121,388,838 'api-design':120,387 'appear':1243 'appli':1485 'area':228 'argument':1343,1357 'arriv':1095 'ask':711 'attr':437 'author':1483 'avoid':508 'awkward':629 'ban':1307 'bannedreason':1298 'base':1443 'belong':740,1105,1518 'block':107 'book':162,332,340,501,666,1161 'bookingdataobject':1156 'bookingmanag':513 'boolean':1308 'boundari':117 'box':1463 'brand':1350 'brush':644,1506 'bug':1150 'busi':1209 'c':419 'call':972 'cancellationpolici':336 'cannot':559 'canon':869,1453 'carri':1279 'cascad':1130 'catch':1324 'chang':254,349 'cheap':269 'choic':246,1424 'choos':20 'chosen':618 'chunk':183 'citat':1559 'class':148,1042 'clean':368 'clean-cod':367 'climat':1348 'close':977 'code':42,369,605,638,817,1283,1492,1502 'codebas':1205,1460 'collect':179,948,1127,1405 'combin':1319 'commit':615 'compet':879,1167 'compil':1331,1360 'concept':18,39,52,111,169,259,272,308,326,487,657,791,802,866,886,909,1061,1173,1380,1394,1426,1457 'configur':811 'consid':788 'consist':732,1121 'constrain':805 'constraint':1134 'construct':671 'constructor':1274,1533 'containskey':568 'cost':781 'count':291 'dan':1499 'dangl':1148 'dangling-refer':1147 'data':208,222,714,1041,1098,1517 'databas':175,203,743,1108,1521 'dataclass':435 'datahelp':975 'dataset':778,1079 'db':1091 'decid':70,180,703,1438 'decis':91,267,466,469,480,929,1023 'decod':541,998 'default':647,1050,1432 'defect':1073 'defin':371 'definit':880,920,1168,1454 'degrad':421 'delet':1131 'deliber':1423 'describ':800 'design':45,122,126,389,592,640,641,1022,1504 'dict':457 'diff':1250 'differ':891,1165 'diomidi':1522 'direct':852 'discrimin':1327 'distil':1558 'doesn':303,1228 'domain':2,17,29,51,159,168,218,235,258,271,307,325,364,378,395,464,491,517,522,680,844,914,952,981,1001,1172,1196,1217,1257,1387,1393,1498,1509 'domain-model':1,463 'domain-specif':1508 'done':1365,1370,1469 'downstream':1282 'drift':1177 'dsl':829,835 'dto':373 'dynam':424 'easier':686,1436 'edward':1489 'effect':1539 'either':632,1470 'email':1267 'emailaddress':1285 'embed':744 'encapsul':1219 'encod':820 'enough':978,1346 'entiti':727 'evalu':231 'evangel':450 'everi':468 'exact':918,1451 'exist':167,234,315,344,363,377,519,888,1160 'expert':492,523,845,982,1388 'explicit':548 'expos':113,1234 'express':810,1396 'f':414 'fail':1358 'featur':227 'field':312,319,1045,1300 'figur':1011 'file':171,192,872 'find':883,907,917 'fine':748,763,1114 'finish':1471 'fire':402 'first':1009,1179 'fit':718 'five':1043 'flag':922,1309 'fluent':837 'follow':1025,1375 'forc':628 'foreign':1128 'forev':784 'form':1557 'fowler':1550 'fowler/primitiveobsession':401,1363,1542 'free':1141 'fresh':672 'frozen':434 'function':289,296,1486 'garson':700,1490 'generic':947 'getter':1047 'give':37,64 'go':1017 'grace':422 'guarante':1174 'guard':391 'hand':765,824,1119,1143 'hand-rol':764,1118,1142 'hardest':403 'hashmap':1082 'haskel':415 'help':443 'helper':263,295 'host':841 'hunger':861,1513 'id':355 'ident':131,660,1064,1428 'immut':649,1052 'implicit':546 'imposs':1334 'in-memori':24,758,1124 'inher':1062 'input':1272 'insid':287 'instead':370,1275 'int':209,212,214,215,353,573,768,770,771,937,939 'interconnect':726,1103,1516 'intern':299,834,944,1225,1231 'internal-on':298 'intrins':658 'introduc':6,48,141,255 'invalid':1318,1526 'invent':509,531,990 'invoic':163 'invok':119,134,135,264,280 'javascript':427 'keep':799,1077 'key':196,1129 'key-valu':195 'kotlin':417 'languag':390,405,425,796,842,1321,1495,1511 'larg':715,1100,1515 'lastmodifi':329 'later':1092,1093 'lead':1070 'least':622,1409 'let':843,1116,1329 'level':13,146 'lifetim':662 'like':745,1367 'line':1190 'link':1562 'littl':795 'live':23,74,187,707,1442 'll':933,971,1005,1076,1153,1261,1295 'local':262,285 'locat':870 'log':1247 'long':357,1556 'long-form':1555 'look':1366 'lookup':945 'make':545,961,1525 'map':211,213,576,767,769,936,938 'mar':1347 'martin':1549 'match':411 'matter':1230 'mean':58,351,925 'memori':26,189,760,1126 'method':79,360,478,552,694,710,968,1008,1024,1035,1197,1214,1400 'michael':1512 'mirror':375 'mismodel':270 'model':3,30,223,236,465 'modern':418 'modul':874 'module/package/service':116 'move':1087 'mutabl':1044,1057,1067,1420 'mutat':675,697 'name':33,53,217,243,485,493,507,528,582,892,987,1227,1238,1253,1382 'need':642,751,792,1193 'nest':575,946,1403 'never':1289 'new':50,143,174,226,257,311,324,328,335,679,1379 'newtyp':440 'next':536,901,994,1157 'non':276 'non-trigg':275 'north':542,586,1500 'nullabl':1299,1316 'object':681,1548 'obsess':571,1544 'old':683 'one':99,288,381,383,684,868,871,873,875,919,1189,1410,1452 'one-lin':1188 'one-to-on':380 'oper':585,676 'orbit':1349 'order':93,471,1344 'outliv':601 'overlap':123 'overview':31 'parallel':885 'pattern':410 'pay':783 'permit':1317 'persist':28,178,721,1101 'pick':598,1240 'place':814,864 'plan':1477 'polici':502 'policyhelp':514 'portfolio':161,500,562,565 'portfolioidsbytraderid.get':567 'pr':1249 'prefer':563,667 'primit':206,570,1404,1543,1546 'primitive-typ':205 'principl':396,1478,1482,1488 'principles.md':1552 'privat':294 'process':723 'produc':678,1146 'program':1487 'programm':510,537,902,995 'proof':1281 'pure':653 'pydant':436 'python':426,454 'queri':809 'ram':720 'rare':1094 'rather':673,1429 'raw':572 'rdbms':1138 're':138,249,530,819,1163,1291,1476 're-encod':818 're-plan':1475 're-valid':1290 'reach':432,1055 'read':846 'real':67 'realist':624 'realiti':931 'realli':775 'reason':689,1419 'receiv':1284 'recogn':526,858,985,1390 'record':150 'recreat':827 'recur':1202 'red':921 'refer':728,1149 'referenc':338 'reflex':1448 'refus':1332 'reject':1414 'relat':202,777 'relationship':547,580,953,963,1391 'renam':9,165,282,292 'replac':204,569,1545 'repres':154,305,1206 'represent':347 'reserv':164 'restart':724,927 'reus':692 'revert':1473 'review':7,242 'right':460 'rigid':87 'roll':766,1120,1144 'rubi':428 'rule':555,733,807,851,1184,1210 'run':89,467 'rust':413 'ryan':1505 'satisfi':98 'say':498 'scala':416 'scenario':625,1032 'scott':1529,1534,1540 'script':455 'search':911 'secret':533,696,957 'see':1551 'servic':230 'serviceutil':976 'set':1304 'setter':1049 'settl':483 'sever':813 'shape':35,596,619,637,1014,1019,1028,1412 'share':698 'singl':284 'size/persistence/interconnectedness':1445 'sketch':220,609,709,1026,1407 'skill':88,1481 'skill-domain-modeling' 'slight':1164 'slow':831 'small':453,750 'smart':1273,1532 'smell':860 'sometim':848 'sourc':1071,1561 'source-oribarilan' 'specif':1510 'spinelli':785,1523 'sqlite':746,1112 'stack':1245 'stand':577,949 'start':855,1111 'state':22,73,185,699,706,1315,1335,1418,1441,1527 'status':1306 'stop':100,893,926 'store':198 'string':210,574,1264,1287,1342 'struct':149,301 'structur':240,761 'sum':407 'sure':251 'surviv':722 'swap':1356 'switch':352 'system':449 'tabl':15,152,176,876 'tacit':956 'tell':102 'term':511,518,915,1002,1385 'test':688 'thing':156 'third':899 'thought':924,930 'three':612,756,878 'time':1066,1361 'timestamp':330 'today':1232 'tomorrow':1235 'top':12,145 'top-level':11,144 'topic-agent-skills' 'topic-ai-agents' 'topic-best-practices' 'topic-claude-code' 'topic-claude-code-plugin' 'topic-claude-code-skills' 'topic-coding-agents' 'topic-copilot-cli' 'topic-copilot-cli-plugin' 'topic-opencode' 'topic-opencode-plugin' 'topic-programming-principles' 'trace':1246 'trader':160,499,558 'trader.canview':564 'transform':654 'transport':385 'treat':589 'trigger':277 'true':1377 'two':610,623,738,1166 'type':14,68,125,147,207,219,245,316,337,345,365,379,394,408,448,476,506,550,583,594,651,669,965,1054,1218,1226,1277,1352,1398,1537 'type-system-evangel':447 'typed-domain':393 'typeddict':438 'typescript':412 'uncheck':1465 'understand':960 'unifi':895,1178 'union':1328 'uniqu':1133 'unless':655 'unrepresent':1528 'use':4,366,386,607,934,999,1262,1325 'user':61,104,354,497,664,798 'userdataobject':512 'userid':1338,1353 'usermanag':974 'valid':616,643,808,1029,1269,1292,1311 'valu':129,197,650,668,1053 'value-vs-ident':128 'variabl':286 've':989 'view':560 'vocabulari':534,806,822,992 'vs':27,130,260 'way':489,832 'well':239 'well-structur':238 'whether':181,232,252,789 'without':348 'wlaschin':1530,1535,1541 'wlaschin/invalidstatesunrepresentable':398,1336,1524 'wlaschin/smartconstructors':399,1293,1531 'wlaschin/typesforeffects':400,1362,1536 'won':716,1322 'workaround':630 'world':63 'would':1389 'wouldn':524,983 'write':77,474,849,1006 'wrong':636 'x':290 'year':1252 'yes':341,735","prices":[{"id":"a94af535-4eea-499f-8c64-71808238cc4f","listingId":"31b4cf1c-28b3-42dc-8e48-a9f3b18c05dd","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"oribarilan","category":"97","install_from":"skills.sh"},"createdAt":"2026-05-08T13:06:23.660Z"}],"sources":[{"listingId":"31b4cf1c-28b3-42dc-8e48-a9f3b18c05dd","source":"github","sourceId":"oribarilan/97/domain-modeling","sourceUrl":"https://github.com/oribarilan/97/tree/main/skills/domain-modeling","isPrimary":false,"firstSeenAt":"2026-05-08T13:06:23.660Z","lastSeenAt":"2026-05-18T19:05:32.825Z"}],"details":{"listingId":"31b4cf1c-28b3-42dc-8e48-a9f3b18c05dd","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"oribarilan","slug":"domain-modeling","github":{"repo":"oribarilan/97","stars":21,"topics":["agent-skills","ai-agents","best-practices","claude-code","claude-code-plugin","claude-code-skills","coding-agents","copilot-cli","copilot-cli-plugin","opencode","opencode-plugin","programming-principles"],"license":"other","html_url":"https://github.com/oribarilan/97","pushed_at":"2026-05-15T21:32:54Z","description":"Agent skills distilled from the hard-won lessons of world-renowned programmers, in the spirit of \"97 Things Every Programmer Should Know\"","skill_md_sha":"3ea63e44d77438b3720275d6af52044d21397306","skill_md_path":"skills/domain-modeling/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/oribarilan/97/tree/main/skills/domain-modeling"},"layout":"multi","source":"github","category":"97","frontmatter":{"name":"domain-modeling","description":"Use when introducing, reviewing, or renaming a top-level type, table, or domain concept; or choosing where state lives (in-memory vs persistent)"},"skills_sh_url":"https://skills.sh/oribarilan/97/domain-modeling"},"updatedAt":"2026-05-18T19:05:32.825Z"}}