Domain Model

Object-Oriented Design with UML

Course 1 ยท Chapter 4 ยท The Domain Model

Chapter 3 described what people want to do. This chapter describes the world those wishes live in: the things the library cares about, and how they relate. That picture is the domain model: a class diagram, but of real-world concepts (a member, a loan, a fine), not of software classes. It is the vocabulary the team will share, and it is the cheapest place to find a misunderstanding, because you can show it to a librarian.

What was run, and what was not
Run on this machine: a word-frequency script over the chapter 3 use cases; a Python model of the library's concepts that produces both the Mermaid diagram (Mermaid 12.1.0, rendered) and the plain-English sentences used to review it; a small Python design with a rule the diagram cannot express; and a demonstration that chapter 3's code has a flaw this model exposes. From general knowledge, not re-read: that this kind of conceptual class diagram is called a "domain model" in Craig Larman's Applying UML and Patterns and a "conceptual" perspective in Martin Fowler's UML Distilled, and Larman's idea of a category list for finding concepts. The library is invented, as before.

Conceptual Model, Design Model

The same notation, drawn for two readers, answers two different questions:

Domain (conceptual) modelDesign model
DescribesThings in the problem worldClasses in the software
ContainsConcepts, attributes, associations, multiplicityAlso operations, visibility, types, interfaces, helper classes
A "Member" class has...name, card number, expiry dateThose, plus methods like borrow() and perhaps a database id
Has a LoanService, a Database, a Controller?No: a librarian has never heard of themYes, probably
Can be reviewed byA librarianA developer
ChaptersThis one5 to 7

The domain model is also not a database schema. It will often map onto one, but its job is understanding, so it ignores keys, indexes, join tables and anything the librarian would not recognise.

Finding the Concepts

Three sources, in order of reliability: the people who know the domain (ask what they call things, and what they confuse), the documents you already have (the use cases), and the existing paper and forms (a library's membership card and fine slip name real concepts). The classic technique is noun and verb analysis: underline the nouns in the requirements as candidate concepts and attributes, and the verbs as candidate relationships.

I ran a crude version over the two use case descriptions from chapter 3 (Borrow a Book and Return a Book): a script that counts the words, ignoring a hand-made list of common words and obvious verbs. There is no language-analysis library on this machine, so this is just counting; the point is the shape of the result:

"""A crude 'noun analysis': which words does the library's requirements text use most? Candidates only: judgement decides.""" import collections import pathlib import re STOP = set("""a an the of to in on at by for with and or if is are be it its this that these those as from not no than then so can has have had will must which who whom what when where how all any each every one two three five 21 new use case ends end step steps system checks check enters enter gives give tells tell records record continues continue refuses refuse offers offer says said saying works out resume resumes main success scenario extensions guarantee precondition trigger scope goal level user primary actor stakeholders hands hand over mark marks notes note mention adds add finds find closes close returns return returned borrow borrows confirms confirm reports report puts put checks check read down up we what too still yet only also may might should would could already instead because about first another other same such own whether""".split()) text = "\n".join(pathlib.Path(f).read_text(encoding="utf8") for f in ("borrow_a_book.usecase.txt", "ex_return.usecase.txt")) words = re.findall(r"[A-Za-z][A-Za-z']+", text) counts = collections.Counter(w.lower() for w in words if w.lower() not in STOP and len(w) > 2) for word, n in counts.most_common(45): print(f"{n:>3} {word}")
19 member 16 librarian 14 book 12 loan 11 copy 6 fine 5 library 5 due 5 date 5 held 4 wants 4 recorded 4 card 4 fines 4 available 3 refused 3 isbn 3 membership 3 open 3 right 3 back 2 lending 2 black 2 box 2 acting 2 desk 2 books 2 signed 2 minimal 2 nothing 2 changed 2 book's 2 valid 2 unpaid 2 limit 2 days 2 loans 2 being 2 hold 2 shelf 1 sea 1 clear 1 their 1 marked 1 known

The top of the list is encouraging: member, librarian, book, loan, copy, fine, card, membership. But it is a list of candidates, and the work is deciding what each word is:

WordDecisionReason
memberConceptHas its own facts and relationships
librarianNot in the modelAn actor: a role of someone using the system (chapter 3). If the library also needs to know which librarian did what, a Staff concept may come back later
book and copyTwo concepts: Title and CopyThe text uses "book" for both an ISBN-identified work and a physical item (see below)
loanConceptA transaction with its own dates; it comes from the verb "borrows"
fineConceptIt has its own amount, date and "paid" status, and arises from a particular loan
due, date, daysAttributes"Due date" is a fact about a loan
card, isbnAttributesIdentifiers of a Member and of a Title (cardNumber, isbn)
membership, validAttribute (expiresOn)Whether it is valid is worked out from a date; it is not a thing
held, hold, shelfA status of a CopyThe hold is a state, not a separate thing. The words are the librarian's, which is why they are in the glossary
available, open, unpaidStatusesValues of an attribute
limit, lendingRules, not concepts"At most 5 loans" is a business rule; it goes in the use case or a constraint
library, desk, box, seaNot in the modelThe library is the scope; the rest is noise (the word "box" came from "black box", "sea" from "sea level")
reservationConceptIt appears only once in the text, so counting would have missed it; I knew it from the stakeholders and from chapter 3's other use cases

Where Noun Analysis Fails

  • It finds words, not meanings. "Book" and "copy" are different concepts hiding behind overlapping words (a later section shows what it cost).
  • It misses what the text rarely says. "Reservation" appears once. Counting would drop it; a stakeholder would not.
  • It cannot tell a concept from an attribute from a status. Every row of the table above needed a human decision. A useful test: if it only has a value (a number, a date, a piece of text), it is an attribute; if it has its own facts or relationships, it is a concept.
  • Plurals and synonyms split the count. "fine" (6) and "fines" (4), "loan" and "loans", "book" and "book's" are counted apart. A real analysis also has to merge "reservation" and "hold" or decide that they differ.
  • Actors and interface words look like concepts. "Librarian" scored second; it does not belong.
  • Verbs hide concepts. "A member borrows a copy" is where Loan came from: the relationship has facts of its own (when, until when, returned or not), so it became a thing. Look at verbs for this.

Larman's alternative (from memory) is a category list: prompt yourself with kinds of thing (physical objects, transactions, roles, places, records, events, catalogues) and ask whether the domain has one of each. Used beside noun analysis it finds what counting misses. Neither replaces asking.

Attributes, Associations and Multiplicity

  • Attributes are the facts a concept holds: a Member has a name, cardNumber, joinedOn, expiresOn. In a domain model keep them to what is meaningful in the domain and leave out types and ids.
  • Associations are meaningful relationships between concepts: "a Member borrows a Loan"; "a Loan is of a Copy". Name each one with a short verb phrase, so the diagram reads as sentences.
  • Multiplicity says how many of one end may relate to one of the other: 1 exactly one, 0..1 optional, 0..* any number, 1..* at least one. Read it as the number of things at that end for each one at the other end.

The Library's Domain Model

Rather than draw the diagram by hand and then write the sentences by hand (and let them drift), I wrote the model once, as data, and a small script produces both. Associations are written as "each LEFT has this many RIGHT".

"""The library domain model as DATA. One source produces the diagram and the plain-English sentences to review with a librarian.""" import pathlib CLASSES = { "Member": ["name", "cardNumber", "joinedOn", "expiresOn"], "Title": ["isbn", "title", "author"], "Copy": ["barcode", "status"], "Loan": ["borrowedOn", "dueOn", "returnedOn"], "Reservation": ["placedOn"], "Fine": ["amountPence", "issuedOn", "paid"], } # (left, left multiplicity, name, right multiplicity, right) read as: each LEFT has <right multiplicity> RIGHT ASSOCIATIONS = [ ("Member", "1", "borrows", "0..*", "Loan"), ("Loan", "0..*", "is of", "1", "Copy"), ("Copy", "0..*", "is a copy of", "1", "Title"), ("Member", "1", "places", "0..*", "Reservation"), ("Reservation", "0..*", "is for", "1", "Title"), ("Member", "1", "owes", "0..*", "Fine"), ("Fine", "0..1", "arose from", "1", "Loan"), ] WORDS = {"0..*": "zero or more", "1": "exactly one", "0..1": "at most one", "1..*": "one or more"} def mermaid(classes=None, associations=None) -> str: classes, associations = classes or CLASSES, associations or ASSOCIATIONS lines = ["classDiagram", " direction LR"] for name, attrs in classes.items(): lines.append(f" class {name} {{") lines += [f" {a}" for a in attrs] lines.append(" }") for left, lm, label, rm, right in associations: lines.append(f' {left} "{lm}" -- "{rm}" {right} : {label}') return "\n".join(lines) + "\n" def _noun(name: str, mult: str) -> str: """Pluralise for 'zero or more'. Done by hand-written rule, so it needs a test (see the Copy case).""" if mult not in ("0..*", "1..*"): return name return name[:-1] + "ies" if name.endswith("y") else name + "s" def sentences(associations=None, reverse=None) -> list[str]: associations, reverse = associations or ASSOCIATIONS, reverse or _reverse out = [] for left, lm, label, rm, right in associations: out.append(f"Each {left} {label} {WORDS[rm]} {_noun(right, rm)}.") out.append(f" ...and each {right} {reverse(label)} {WORDS[lm]} {_noun(left, lm)}.") return out def _reverse(label: str) -> str: return {"borrows": "is borrowed by", "is of": "is the subject of", "is a copy of": "has as a copy", "places": "is placed by", "is for": "is wanted by", "owes": "is owed by", "arose from": "gave rise to"}[label] if __name__ == "__main__": pathlib.Path("library.domain.mmd").write_text(mermaid(), encoding="utf8") print("\n".join(sentences()))

borrows

1

0..*

is of

0..*

1

is a copy of

0..*

1

places

1

0..*

is for

0..*

1

owes

1

0..*

arose from

0..1

1

Member

name

cardNumber

joinedOn

expiresOn

Title

isbn

title

author

Copy

barcode

status

Loan

borrowedOn

dueOn

returnedOn

Reservation

placedOn

Fine

amountPence

issuedOn

paid

Mermaid, generated by model.py. Lines are plain associations: at the domain level, who can navigate to whom is a design decision, not yet made. The diagram is wide; the text is easier to read in the sentences below.

Reviewing It With a Non-Programmer

A librarian will not read UML, and does not need to. Read the model aloud as sentences; the script's output, verbatim:

Each Member borrows zero or more Loans. ...and each Loan is borrowed by exactly one Member. Each Loan is of exactly one Copy. ...and each Copy is the subject of zero or more Loans. Each Copy is a copy of exactly one Title. ...and each Title has as a copy zero or more Copies. Each Member places zero or more Reservations. ...and each Reservation is placed by exactly one Member. Each Reservation is for exactly one Title. ...and each Title is wanted by zero or more Reservations. Each Member owes zero or more Fines. ...and each Fine is owed by exactly one Member. Each Fine arose from exactly one Loan. ...and each Loan gave rise to at most one Fine.

Every sentence is a claim the librarian can accept or reject, and a few of these are not obviously right, which is the point. "Each Fine arose from exactly one Loan": what about a fine for a lost book, or for a late payment of something else? "Each Reservation is for exactly one Title": could a member ask for a specific edition? "Each Member owes zero or more Fines": is a fine ever paid in part? I recorded these as open questions rather than answering them myself. Showing sentences instead of boxes also hides nothing: a librarian reading "each Title has as a copy zero or more Copies" notices that a title with no copies is allowed, and can say whether a title nobody owns any more should stay on record.

A Glossary

A domain model is only useful if the whole team uses its words the same way, so write them down. One line per term, in the domain's own language, plus the words you decided not to use:

GLOSSARY: Library lending system (draft 2026-10-10: to be confirmed with the librarian) Title A published work, identified by its ISBN. "We have Dune." Not a physical object. Copy One physical item that can be lent, identified by its barcode. "We have three copies of Dune." (Never just "book": the word means Title in one sentence and Copy in the next.) Member A person registered to borrow. Identified by card number. Has a membership with an expiry date. Loan A Member having a Copy, from the day it is borrowed until it is returned. One Copy has at most one open Loan. Reservation A Member's request to be first in the queue for the next available Copy of a Title. Not for a particular Copy. Fine An amount a Member owes because a Loan came back late. Hold The state of a returned Copy kept aside for the Member who is first in a Title's reservation queue. (A status of a Copy, not a separate thing: see "Copy.status".) Librarian A role played by library staff using the system. An actor in the use cases; not part of the domain model.

The glossary entry for Copy says "never just 'book'", and that rule came from a real mistake, below.

What the Model Found in Chapter 3

In chapter 3 the Borrow use case, and the Python written from it, identified the item being lent by ISBN. The domain model says an ISBN identifies a Title, and the library may own several Copies of one title. Here is chapter 3's code with two physical copies of ISBN 111 on the shelf and two members:

physical copies owned: 2 copies the chapter-3 code knows about: 1 second member, second copy on the shelf, but: on loan: offer 'Reserve a book'

The code's dictionary of copies is keyed by ISBN, so the two copies collapse into one, and the second member is told the book is on loan when a copy is sitting on the shelf. This is not a coding slip; it is a vocabulary error in the requirement, which the model's two separate boxes make obvious. Chapter 3's page now records it. The fix belongs in the use case ("the copy's barcode") and in the model, not in the dictionary.

What Multiplicity Cannot Say

The diagram says a Copy can have 0..* Loans. That is true over time. What it cannot say is the rule that matters: at most one of them may be open at once. That is a constraint, and it belongs beside the diagram as a sentence (UML has a formal constraint language, OCL, which I do not teach here). Here is the model as a first plain Python design, with the rule enforced, and a run that shows both halves:

"""The domain model as a first, plain Python design. Names and associations come from the diagram; nothing else is decided yet.""" from dataclasses import dataclass, field from datetime import date @dataclass class Title: # a published work: what the ISBN identifies isbn: str title: str author: str @dataclass class Copy: # one physical item on one shelf: what the barcode identifies barcode: str title: Title # Copy "0..*" -- "1" Title status: str = "available" @dataclass class Loan: copy: Copy # Loan "0..*" -- "1" Copy borrowed_on: date due_on: date returned_on: date | None = None @dataclass class Member: name: str card_number: str loans: list[Loan] = field(default_factory=list) # Member "1" -- "0..*" Loan def open_loans(self) -> list[Loan]: return [l for l in self.loans if l.returned_on is None] # A rule the multiplicities cannot say: Copy "0..*" -- Loan allows many loans per copy (over time), but at most ONE of them open. def lend(member: Member, copy: Copy, today: date, due_on: date, all_members: list[Member]) -> Loan: for m in all_members: for l in m.open_loans(): if l.copy is copy: raise ValueError(f"copy {copy.barcode} is already on loan to {m.name}") loan = Loan(copy, today, due_on) member.loans.append(loan) return loan
from datetime import date from domain import Copy, Member, Title, lend dune = Title("978-0-00-000000-0", "Dune", "F. Herbert") copy1, copy2 = Copy("B-001", dune), Copy("B-002", dune) ada, bo = Member("Ada", "M1"), Member("Bo", "M2") members = [ada, bo] today, due = date(2026, 10, 10), date(2026, 10, 31) lend(ada, copy1, today, due, members) lend(bo, copy2, today, due, members) print("two members borrowed Dune at once, using two copies of one title:", [(m.name, l.copy.barcode, l.copy.title.title) for m in members for l in m.loans]) try: lend(bo, copy1, today, due, members) except ValueError as e: print("the same copy twice:", e) ada.loans[0].returned_on = today lend(bo, copy1, today, due, members) print("after Ada returns it, Bo can borrow that copy; copy1 has", sum(1 for m in members for l in m.loans if l.copy is copy1), "loans in its history, 1 open")
two members borrowed Dune at once, using two copies of one title: [('Ada', 'B-001', 'Dune'), ('Bo', 'B-002', 'Dune')] the same copy twice: copy B-001 is already on loan to Ada after Ada returns it, Bo can borrow that copy; copy1 has 2 loans in its history, 1 open

Two members borrow Dune at once with two copies of one title, which the old ISBN-keyed code could not do. The same copy cannot go out twice, and can go out again after it returns, leaving two loans in its history with one open. A domain model plus its constraints is more than the picture; keep a short list of such rules next to it.

Mistakes while preparing this chapter
  • My own earlier chapter had the wrong vocabulary. Chapter 3 treated an ISBN as the identity of a physical book. What went wrong: I used the word "book" for two concepts and never asked which. How to avoid it: before writing flows, write the glossary; and when a single noun is used in two senses ("we have the book"/"the book is on loan"), split it.
  • "Copys". My generated sentences read "each Title has as a copy zero or more Copys", because my pluraliser just added an "s". It is a small thing in a document the librarian is meant to read aloud, and an embarrassing one. I replaced it with a rule for words ending in "y". Takeaway: read generated text, and test hand-written pluralisation on the words your model really uses.
  • A word count mistaken for analysis. My first instinct was to treat the top of the frequency list as the concept list. It contains an actor (librarian), two overlapping words (book, copy), statuses (held, available, open) and rules (limit), and misses "reservation". The table above is the real work. Takeaway: a tool that produces candidates is a prompt for judgment, not a replacement.
  • Mermaid text does not say who reads it. The diagram has no arrowheads on purpose; at the domain level, navigation is undecided. Drawing arrows too early would have claimed a design decision (that a Copy cannot find its Loans) I had not made.

Hands-On Exercises

Exercise 1

Here is a short description of a bike repair shop (in the solution file). Run a word count over it, then classify every candidate as concept, attribute, status, rule, actor or noise, and say what the counting missed or got wrong.

๐Ÿ“„ View solution
Exercise 2

Write the bike shop's domain model as data (concepts with attributes, associations with multiplicities), generate the Mermaid diagram and the review sentences, render it, and name two sentences you would ask the shop owner about.

๐Ÿ“„ View solution
Exercise 3

The bike shop has a rule its multiplicities cannot express: a bike can have only one job in progress at a time. Write it as a constraint in words, enforce it in Python, and write tests showing that many jobs over time are fine, two at once are refused, and finishing a job frees the bike. Run them.

๐Ÿ“„ View solution

Chapter 4 Quick Reference

  • Domain model = class diagram of real-world concepts: attributes, associations, multiplicity; no operations, helper classes or database detail
  • Sources: domain experts, use cases, existing forms; noun analysis for concepts and attributes, verbs for relationships (and for hidden concepts like Loan); a category list as a second prompt
  • Concept vs attribute: if it only has a value, it is an attribute; if it has its own facts or relationships, it is a concept
  • Noun analysis is a source of candidates: it misses rarely mentioned things, confuses actors, statuses and rules with concepts, and cannot see that one word means two things
  • Multiplicity: 1, 0..1, 0..*, 1..*; read as "for each one at the other end, how many at this end"
  • Multiplicity cannot express "at most one open at a time": write such rules as constraints beside the diagram and enforce them in code
  • Review with non-programmers by reading associations as sentences; keep a glossary, including the words not to use
  • A domain model usually corrects the use cases written before it; expect to go back
Coming next
Object-Oriented Design with UML 5 (Class Diagrams in Depth): the full notation (visibility, static, abstract, interfaces), association versus aggregation versus composition, dependency, generalisation and realisation, each with a runnable code example, navigability, association classes and the usual mistakes.