
CS 3100: Program Design and Implementation II
Lecture 12: Domain Modeling
©2026 Jonathan Bell & Ellen Spertus, CC-BY-SA
Learning Objectives
After this lecture, you will be able to:
- Formulate a domain model given a set of requirements
- Validate domain models with stakeholders through scenario walkthroughs
- Translate a domain model into an object-oriented design using responsibility assignment heuristics
- Evaluate the representational gap between a domain model and an OO design
Poll: Which code is easier to understand, and why?
Option A:
public double process(String id, double amt, String type) {
double curr = balances.getOrDefault(id, 0.0);
double fee = type.equals("PREMIUM") ? 0.0 : amt * 0.02;
double result = curr - amt - fee;
if (result < 0) throw new RuntimeException("insufficient");
balances.put(id, result);
return result;
}
Option B:
public Balance withdraw(Account account, Money amount) {
Money fee = account.membership().calculateFee(amount);
Balance newBalance = account.balance().subtract(amount).subtract(fee);
if (newBalance.isNegative()) throw new InsufficientFundsException(account);
account.updateBalance(newBalance);
return newBalance;
}
Requirements Are the Input—Domain Modeling Is the Output
In Lecture 9, we learned to gather and analyze requirements:
- Stakeholder interviews and observation
- Functional vs. non-functional requirements
- User stories and acceptance criteria
But requirements are just the input. Today we learn what to do with them.
Domain modeling transforms messy real-world requirements into a structured understanding that guides our code.
Understandability Is the Ultimate Goal of Good Design
Throughout this course, we've emphasized changeability, readability, and maintainability. But these all serve a more fundamental goal:
Understandability
Code that humans can understand is code that humans can successfully work with, modify, and trust.
But understandability is subjective—it depends on context. One common approach: model the problem domain in the code.
Real Scenarios Reveal Why Domain Modeling Matters
A student submits their assignment. A grader starts reviewing it. But then the student realizes they forgot something and resubmits.
How should our system handle this?
Let's look at two implementations...
Technical-Focused Design Hides Meaning in Implementation Details
public class SubmissionManager {
private Map<String, List<byte[]>> fileStorage = new HashMap<>();
private Map<String, Integer> versionCounters = new HashMap<>();
private Map<String, Map<String, Object>> gradeData = new HashMap<>();
public String submitFiles(String userId, String assignmentId, List<byte[]> files) {
String key = userId + "_" + assignmentId;
Map<String, Object> existingGrade = gradeData.get(key);
if (existingGrade != null && existingGrade.get("status").equals("IN_PROGRESS")) {
int version = versionCounters.getOrDefault(key, 0) + 1;
versionCounters.put(key, version);
fileStorage.put(key + "_v" + version, files);
Map<String, Object> newGradeData = new HashMap<>(existingGrade);
newGradeData.put("status", "NEEDS_REGRADING");
gradeData.put(key + "_v" + version, newGradeData);
return key + "_v" + version;
}
fileStorage.put(key, files);
return key;
}
}
Domain-Aligned Design Makes Code Read Like the Business Process
public class Submission {
private final Student student;
private final Assignment assignment;
private final List<SourceFile> files;
private GradingSession activeGradingSession;
public Submission createRevision(List<SourceFile> newFiles) {
Submission revision = new Submission(student, assignment, newFiles);
if (hasActiveGrading()) {
GradingDraft draft = activeGradingSession.convertToDraft();
revision.attachDraftGrading(draft);
activeGradingSession.getGrader().notify(
new RevisionSubmittedEvent(this, revision)
);
}
return revision;
}
public boolean hasActiveGrading() {
return activeGradingSession != null && !activeGradingSession.isComplete();
}
}
Domain-Aligned Code Is Easier to Navigate, Predict, and Change
Approach A Problems:
- What does "v2" mean?
- Why concatenate strings for keys?
- What happens to grader's work?
- What triggers "NEEDS_REGRADING"?
Approach B Clarity:
Submissionhas aGradingSessionGradingSession→GradingDraft- Grader is notified explicitly
- Code reads like business process
Business logic: the rules, workflows, and decisions that define how a domain actually operates—independent of how we store or display data.
When code uses the same concepts, relationships, and vocabulary as the problem domain, it becomes easier to:
- Navigate from requirement to implementation
- Predict where functionality lives
- Understand the impact of changes
- Communicate with non-technical stakeholders
Code Is For Humans First

"Programs must be written for people to read, and only incidentally for machines to execute."
— Harold Abelson & Gerald Sussman, Structure and Interpretation of Computer Programs, 1984
Stakeholders Use the Same Words to Mean Different Things
A "submission" means different things to different stakeholders:
| Stakeholder | "Submission" means... |
|---|---|
| Student | "My attempt at the assignment" |
| Grader | "Work I need to evaluate" |
| System | "An execution of grading script on student code" |
Domain modeling forces us to be explicit about our understanding before making implementation decisions.
Formulating a Domain Model: A 4-Step Process
Creating a domain model is iterative discovery and refinement:
- Extract candidate concepts from requirements
- Filter and consolidate—remove synonyms, distinguish attributes from concepts
- Discover hidden concepts implied but not named
- Build the initial model with relationships
Nouns Become Classes; Heavy Verbs Become Interfaces
Nouns → Classes or Fields
| In requirements | Becomes |
|---|---|
| "students", "graders" | Classes (they have identity, behavior) |
| "email", "due date" | Fields (just data on something else) |
| "score" | Depends! Could be either. |
Verbs → Methods... or Classes
| In requirements | Becomes |
|---|---|
| "submit", "grade" | Methods (usually) |
| "request regrade" | A class! (it has lifecycle, data) |
| "ensure fair distribution" | Strategy object (remember L7?) |
When a verb has a lifecycle, carries data, or varies in implementation → promote it to a class (or interface!).
Example: Pawtograder Grading System Requirements
The university wants to modernize how graders provide feedback on programming assignments. Graders can provide inline comments on specific lines of code, rate code quality on various dimensions (correctness, style, design), and assign scores based on a rubric. Some graders ("meta-graders") have additional privileges—they can review other graders' work and handle regrade requests.
Students can request regrades if they believe their submission was scored incorrectly. The original grader handles the initial regrade request. If the student is still dissatisfied, they can escalate to a meta-grader. As a last resort, the instructor can review any grading decision.
The system needs to ensure fair distribution of grading work and prevent the same grader from repeatedly grading the same student (to avoid bias). Meta-graders should review a sampling of each grader's work to ensure consistency. All grading actions must be tracked for audit purposes.
Step 1: Extract Candidate Concepts
Read the requirements like a detective. Every noun could be a class:
The university wants to modernize how graders provide feedback on programming assignments. Graders can provide inline comments on specific lines of code, rate code quality on various dimensions (correctness, style, design), and assign scores based on a rubric. Some graders ("meta-graders") have additional privileges...
Students can request regrades if they believe their submission was scored incorrectly. The original grader handles the initial regrade request. If the student is still dissatisfied, they can escalate to a meta-grader...
Don't filter yet! "Lines of code" might become a class. "Dimensions" might not. We decide later.
Step 2: Filter and Consolidate
Now we sculpt. Not everything survives:
Merge synonyms — pick one name and stick with it:
programming assignments + assignments → Assignment ✓
Demote to attributes — not everything deserves to be a class:
lines of code → just an int lineNumber on InlineComment
Spot variation — where things get interesting:
Grader vs Meta-grader... inheritance? role attribute? We'll decide.
Step 3: Discover Hidden Concepts
The requirements never mention these. But read between the lines:
📝 "ensure fair distribution of grading work"
→ Who distributes? A GradingAssignment or WorkloadManager
📝 "prevent same grader from repeatedly grading same student"
→ Someone tracks history. A GradingHistory or constraint object.
📝 "tracked for audit purposes"
→ An AuditLog or GradingEvent we never discussed.
⚠ Hidden concepts often become the most architecturally critical classes. Miss them, and logic scatters everywhere.
Step 4: Assemble the Model
From filtered concepts and hidden discoveries, our first sketch emerges:
Actions With Lifecycles Deserve Their Own Classes
When an action carries weight, it deserves to be a class:
// Starting point: "regrade" as a method
class Grader {
void regradeSubmission(Submission s) { }
}
// Leveled up: "RegradeRequest" captures the full lifecycle
class RegradeRequest {
Student requester;
GradingSession originalSession;
String reason;
RegradeStatus status;
List<RegradeResponse> responses; // Can have multiple as it escalates
}
Promote when: the action has a lifecycle, carries data, or needs to be referenced later.
Lifecycles
An object has a lifecycle if it moves through different states over time, affecting which actions are possible and how they behave.
RegradeRequest example
- Submitted → student has requested a regrade
- UnderReview → grader, meta-grader, or instructor is looking at it
- Resolved → grader made a decision
- Escalated → student appeals decision to next level
- Closed → either
- highest authority has performed review
- student did not object to resolution in allowed time
Not Everything Deserves to Be a Class
Not everything deserves to be a class. Find the right level:
// Too fine-grained:
class LineNumber { private int value; }
class CommentText { private String value; }
// Right level: primitives for simple values, classes for real concepts
class InlineComment {
int lineNumber; // Just an int
String comment; // Just a String
SourceFile file; // This IS a concept worth modeling
}
Make it a class when: it has behavior, invariants to protect, or domain experts talk about it as a distinct thing.
Validate Early: Misunderstandings Are Cheaper to Fix Before Code
A domain model is a communication tool. Before writing code, validate your understanding with stakeholders.
Validation exposes critical misunderstandings early, when they're cheap to fix.
Key validation techniques:
- Scenario walkthroughs
- Terminology review
- Multiplicity verification
- Missing concepts discovery
Walk Through Scenarios to Expose Hidden Assumptions
Take specific scenarios and trace them through the model:
"A student is unhappy with their grade and requests a regrade. According to our model, they create a RegradeRequest linked to their GradingSession. The original Grader creates a RegradeResponse. If the student is still unsatisfied, what happens?"
This might expose issues like:
- "Students can't escalate directly to instructors—it must go through meta-graders first"
- "We need a cooling-off period—students can't request a regrade within 24 hours"
- "Meta-graders can't handle regrades for submissions they already reviewed"
Use the Language Stakeholders Already Use
Ensure you're using the right words:
You: "We're calling them 'meta-graders'. Is that the right term?"
Professor: "Actually, we call them 'head TAs' and regular graders are just 'TAs'."
This reveals we should align our model with existing terminology.
Using unfamiliar terms creates a translation burden. Use the language stakeholders already use.
Validation Uncovers Business Rules You Never Knew Existed
| Category | Example discovery |
|---|---|
| Hidden business rules | "Regrades can only adjust scores up to 10% without instructor approval" |
| Temporal constraints | "Grading must be completed within one week of submission deadline" |
| Hidden actors | "The department administrator can override grading assignments" |
| Complex states | "A regrade isn't just 'pending' or 'resolved'—it can be 'awaiting-response', 'under-review', 'escalated', 'requires-meeting'" |
| Cross-cutting concerns | "We need to track TIME spent on grading for workload credits" |
The model after validation often looks quite different from the initial attempt—but it much better represents the actual problem.
Responsibility Assignment Determines Code Structure
Now we translate our validated domain model into working code.
The critical question: Which classes own which behaviors?
We'll use three key heuristics:
- Information Expert — Who has the data needed?
- Creator — Who should create new objects?
- Controller — Who coordinates complex operations?
Assign Behavior to the Class That Has the Data
Assign responsibility to the class that has the information needed to fulfill it.
Example: Who should determine if a regrade can be escalated?
❌ Ignoring the heuristic:
// Service must extract all data from request
class RegradeService {
boolean canEscalate(RegradeRequest req) {
List<RegradeResponse> responses =
req.getResponses(); // extract
RegradeStatus status =
req.getStatus(); // extract
if (responses.isEmpty()) return false;
if (status == RESOLVED) return false;
// ... more extraction and logic
}
}
Service becomes a "logic hog" that pulls data from passive objects.
✓ Following the heuristic:
// RegradeRequest knows its own history
class RegradeRequest {
private List<RegradeResponse> responses;
private RegradeStatus status;
boolean canEscalate() {
if (responses.isEmpty()) return false;
if (status == RESOLVED) return false;
// Data is RIGHT HERE—no extraction
return coolingOffPeriodPassed();
}
}
Data and behavior stay together. Self-contained.
The Class That Knows Its Constraints Should Enforce Them
Who should check if a grader can review a regrade?
❌ Logic scattered in service:
class RegradeService {
boolean canGraderReview(Grader g, RegradeRequest r) {
// Pull data from grader
GraderType type = g.getType();
int activeCount = g.getActiveGradingCount();
int max = g.getMaxConcurrentGradings();
// Pull data from request
Grader original = r.getOriginalSession()
.getGrader();
// Service implements the logic
if (original.equals(g)) return false;
if (activeCount >= max) return false;
return true;
}
}
Service knows too much about Grader's internals.
✓ Grader is the expert on itself:
class Grader {
private GraderType type;
private int activeGradingCount;
boolean canReviewRegrade(RegradeRequest req) {
// Can't review own work
if (req.getOriginalSession()
.getGrader().equals(this)) {
return false;
}
// Check MY workload (I know it!)
if (activeGradingCount >= getMax()) {
return false;
}
return true;
}
}
Grader knows its own constraints. Easy to test.
Containers Should Create What They Contain
Assign class B the responsibility to create instances of class A if B aggregates, contains, or has the initializing data for A.
Example: Who should create InlineComment objects?
❌ Ignoring the heuristic:
// External service creates comments
class CommentService {
InlineComment createComment(
GradingSession session,
SourceFile file, int line, String text) {
InlineComment c = new InlineComment(
session, file, line, text, now());
session.getComments().add(c); // Reaches in!
return c;
}
}
Service reaches into GradingSession's internals. Session loses control of its own state.
✓ Following the heuristic:
// Container creates what it contains
class GradingSession {
private List<InlineComment> comments = ...;
InlineComment addComment(
SourceFile file, int line, String text) {
if (!isActive()) throw new ...;
InlineComment c = new InlineComment(
this, file, line, text, now());
comments.add(c);
return c;
}
}
GradingSession controls its own contents. Can enforce invariants (must be active).
Services Create When Coordination Is Required
For complex creation involving multiple entities, a service may be the creator:
public class RegradeService {
private final NotificationService notificationService;
private final AuditLog auditLog;
public RegradeResponse handleRegradeRequest(
Grader grader, RegradeRequest request,
Decision decision, String explanation,
List<RubricScore> adjustedScores) {
if (!grader.canReviewRegrade(request)) {
throw new UnauthorizedRegradeException();
}
// Service creates RegradeResponse with all necessary data
RegradeResponse response = new RegradeResponse(
grader, request, decision, explanation, adjustedScores
);
request.addResponse(response);
auditLog.recordRegradeResponse(grader, request, response);
notificationService.notifyStudent(request.getStudent(), response);
return response;
}
}
Controllers Coordinate—They Don't Implement Business Logic
Assign responsibility for handling system events to a controller class that coordinates—not implements—the response.
❌ Fat controller (logic in controller):
void escalate(RegradeRequest request) {
// Business logic IN the controller!
if (request.getResponses().isEmpty()) {
throw new BadRequestException("No responses");
}
if (request.getStatus() == RESOLVED) {
throw new BadRequestException("Already resolved");
}
RegradeResponse last = request.getResponses().getLast();
if (hoursBetween(last.getCreatedAt(), now()) < 24) {
throw new BadRequestException("Wait 24h");
}
request.setStatus(ESCALATED); // More logic...
}
✓ Thin controller (delegates to domain):
void escalate(RegradeRequest request) {
if (!request.canEscalate()) { // Domain decides!
throw new EscalationNotAllowedException();
}
regradeService.escalateRequest(request);
}
Controller coordinates. Domain objects own business rules.
Example: Escalating a Regrade Request
Each Class Has One Clear Job
// Information Expert: Domain objects know about themselves
public class RegradeRequest {
public boolean canEscalate() { /* uses its own data */ }
public void escalate() { /* uses its own data */ }
}
// Creator: Containers create their components
public class GradingSession {
public InlineComment addComment(SourceFile f, int line, String text) {
/* creates and contains comments */
}
}
// Creator: Services create complex aggregates
public class RegradeService {
public RegradeResponse handleRegradeRequest(...) {
/* coordinates creation with notifications, audit */
}
}
// Controller: Coordinates without business logic
public class RegradeController {
public Response escalate(...) {
/* authenticate, authorize, delegate, update GUI */
}
}
Poll: Where Should This Logic Live?
"Check if a student has exceeded their maximum regrade requests for the semester"
A. Student
B. RegradeRequest
C. RegradeService
D. RegradeController
E. A new RegradePolicy class

Some Gap Between Domain and Implementation Is Inevitable

Technical Infrastructure Creates the Gap
Our implementation requires technical code absent from the domain model:
Saving and Loading Data
// Domain model has no concept of:
class RegradeStorage {
void save(RegradeRequest request) {
// Write to file? Send to server?
// Domain doesn't care!
}
RegradeRequest load(String id) {
// Read from somewhere...
}
}
Map<String, Object> toJson(RegradeRequest r) {
// Convert domain object to JSON
// for sending over the network
}
Performance Optimization
// Domain: "Grader has graded many students"
// Simple but slow:
class Grader {
Set<Student> gradedStudents; // Thousands!
}
// Actual implementation:
class Grader {
// Only keep recent ones in memory
Set<String> recentStudentIds;
boolean hasGradedStudent(String studentId) {
if (recentStudentIds.contains(studentId))
return true;
return loadFromStorage(studentId);
}
}
Rich Domain Models Keep Business Logic Close to Data
Put behavior in domain classes, not just data:
Anemic Model (larger gap)
public class RegradeRequest {
private List<RegradeResponse> responses;
private RegradeStatus status;
// Just getters and setters
public List<RegradeResponse> getResponses() {
return responses;
}
public void setStatus(RegradeStatus s) {
this.status = s;
}
}
// Business logic lives elsewhere
class RegradeService {
boolean canEscalate(RegradeRequest r) {
// Logic here, far from data
}
}
Rich Model (smaller gap)
public class RegradeRequest {
public boolean canEscalate() {
// Business logic lives WITH the data
return hasResponse() &&
!isResolved() &&
hasCoolingOffPeriodPassed();
}
public void escalate(Grader newReviewer) {
if (!canEscalate()) {
throw new EscalationNotAllowedException();
}
this.status = RegradeStatus.ESCALATED;
this.currentReviewer = newReviewer;
}
}
Hide Storage Details to Enable Future Change
When you need to save/load data, use method names that speak domain language:
❌ Technical language exposed:
class RegradeService {
void handleEscalation(String id) {
// Caller sees storage details
String json = fileSystem.read(
"regrades/" + id + ".json");
RegradeRequest r = parseJson(json);
// ...
}
}
✓ Domain language only:
class RegradeService {
void handleEscalation(String id) {
// Storage is hidden
RegradeRequest r =
storage.findPendingRequest(id);
// ...
}
}
Domain vocabulary: findPendingRequest, not readJsonFile. Storage is a likely axis of change.
A Domain Expert Should Understand Your Core Classes
Ask these questions to evaluate your representational gap:
- Can a domain expert read your core classes and understand them?
- Do method names use domain vocabulary or technical jargon?
- Is domain logic mixed with technical concerns (persistence, HTTP)?
- How many layers between user story and implementation?
- Could you explain the code structure by sketching the domain model?
Some gap is inevitable. The goal is not zero gap, but ensuring the gap exists only where necessary and is well-contained.
Domain Models Are Expensive to Change—Choose Wisely
Domain models are expensive to change once code depends on them.
Every domain model embeds assumptions that may need revision:
- Whose vocabulary? A medical system modeling "patient compliance" may shift to "patient autonomy"
- Whose categories? A system with boolean
isMalewill need refactoring for broader gender representation - Whose workflows? A recipe app designed around cookbooks may struggle with video integration
Domain concepts propagate through the codebase—into method names, class hierarchies, database schemas, API contracts.
Small Assumptions Can Have Catastrophic Consequences
In December 2004, regional airline Comair experienced a catastrophic scheduling system failure caused by a 16-bit signed integer counter tracking crew assignments.
The counter reset annually with a maximum value of 32,767. Bad weather combined with airline expansion meant unusually high scheduling activity. The counter overflowed.
Result: Three consecutive days of complete flight cancellations (December 24-26). The airline never fully recovered.
The worst part? An updated version fixing the overflow was already available—Comair declined to migrate because the upgrade seemed "too expensive."
A seemingly trivial decision—"16 bits should be plenty"—became an existential threat.
Domain Modeling Is Where All the Principles Come Together
| Domain Modeling Step | Uses This Principle | From Lecture |
|---|---|---|
| Extract nouns → classes | Encapsulation, Abstraction | L1, L2 |
| Filter with SRP | Single Responsibility | L8 |
| Hidden concepts → Strategy | Strategy Pattern, Open/Closed | L5, L7, L8 |
| Validate with stakeholders | Requirements Analysis | L9 |
| Information Expert | Cohesion, Information Hiding | L6, L7 |
| Creator heuristic | Encapsulation, Invariant Protection | L6 |
| Rich domain model | Behavior with data, not anemic objects | L6, L7 |
Domain modeling isn't a new topic—it's where all the principles come together.
Key Takeaways
- Domain modeling transforms requirements (L9) into code structure
- Four-step process: Extract → Filter (SRP) → Discover hidden concepts (Strategy) → Build
- Validate early with stakeholders—cheaper than fixing later
- Responsibility heuristics apply Information Expert (L6) and cohesion (L7)
- Minimize the gap between domain understanding and implementation
Good OO design creates code that reflects human understanding of the problem—not just technical machinery.
Looking Ahead
Domain modeling connects to many upcoming topics:
| Lecture | Connection |
|---|---|
| L19: Software Architecture | Domain models inform system boundaries |
| L20: Design Patterns | Patterns implement domain concepts |
| L35: Safety and Reliability | Domain assumptions become safety requirements |
When we succeed, our code becomes a precise yet understandable expression of the solution to real-world problems.
Bonus Slide

Optional Practice Exercises
The following slides contain practice exercises for applying domain modeling concepts to a library hold system.
These exercises are not required during lecture time. They are provided for:
- Lab activities and discussion
- Homework practice
- Self-study and review
Practice Example: Library Hold System
The library wants to modernize how patrons request books that are currently checked out. Patrons can place holds on unavailable items, and when an item is returned, the next patron in the queue is notified. They have 7 days to pick up the item before the hold expires and the next patron is notified.
Some books have multiple copies; when any copy is returned, it fulfills the next hold. Patrons can cancel holds anytime and can have at most 10 active holds.
Exercise: Select Candidate Concepts
Choose nouns to be classes. They could be explicit or implicit.
The library wants to modernize how patrons request books that are currently checked out. Patrons can place holds on unavailable items, and when an item is returned, the next patron in the queue is notified. They have 7 days to pick up the item before the hold expires and the next patron is notified.
Some books have multiple copies; when any copy is returned, it fulfills the next hold. Patrons can cancel holds anytime and can have at most 10 active holds.
Exercise: One Possible Domain Model
Exercise Poll: Who should create Copy objects?
A. Book
B. Copy
C. Hold
D. Patron
Heuristic: Assign responsibility to the class that has the information needed to fulfill it.
Exercise: Creating Copy Objects
In Book (contains copies):
class Book {
private List<Copy> copies;
Copy addCopy(String barcode) {
Copy copy = new Copy(barcode, this);
copies.add(copy);
return copy;
}
}
In Copy (doesn't contain copies):
class Copy {
private String barcode;
private Book book;
static Copy create(String barcode, Book book) {
Copy copy = new Copy(barcode, book);
book.getCopies().add(copy); // reaching in!
return copy;
}
}
Copy must reach into Book's internal list.
Book can manage its own collection.
Exercise Poll: Who should check if a patron can place a hold?
A. Book
B. Copy
C. Hold
D. Patron
Heuristic: Assign responsibility to the class that has the information needed to fulfill it.
Exercise: Checking if a Patron Can Place a Hold
In Patron (has the data):
class Patron {
private List<Hold> activeHolds;
boolean canPlaceHold(Book book) {
if (activeHolds.size() >= 10) return false;
for (Hold h : activeHolds) {
if (h.getBook().equals(book)) return false;
}
return true;
}
}
In Hold (doesn't have the data):
class Hold {
private HoldStatus status;
boolean canPatronPlaceHold(Patron p, Book b) {
// I don't know about other holds!
// Need to ask Patron for their list
List<Hold> holds = p.getActiveHolds();
if (holds.size() >= 10) return false;
for (Hold h : holds) {
if (h.getBook().equals(b)) return false;
}
return true;
}
}
Hold must extract data from Patron to answer the question. Patron already has it.