Mini Capstone in Domain Modeling
Ruby source files are converted directly from the English source tree.
en/topic_09_mini_capstone
Overview
Why this topic matters ๐ก
The capstone exists to gather the course ideas into one small domain. Students move from isolated features to a coherent miniature system with domain objects, state changes, validation, collaboration, and export behavior.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- model a small domain with objects that have distinct responsibilities;
- preserve domain rules such as borrowing limits;
- compose services around existing domain objects instead of collapsing everything into one class;
- use a duck-typed exporter to separate representation from domain logic;
- explain how the capstone demonstrates the broader Ruby style taught across the course.
Assessment focus โ
Students should be able to explain why the model is split across Book, Member, Library, and LibraryReport.
Short Note
A capstone should not be large. It should be large enough to reveal whether the student can connect the ideas from earlier topics:
- clear object responsibilities;
- validation and domain rules;
- readable state changes;
- collaboration through composition;
- extension through duck-typed exporters.
Ruby beauty in this topic:
- small objects can model a meaningful workflow clearly;
- business rules can stay close to the objects they belong to;
- alternate output behavior can be added without rewriting the core domain.
Ruby caution in this topic:
- tiny domains still become messy if responsibility boundaries are weak;
- "simple" code becomes procedural quickly if everything is pushed into one class.
Reflection prompt:
- Which object owns which rule, and why?
Worked Examples
Example 1: Borrowing rule on the member ๐ก
The borrowing limit belongs to Member, not to a controller-like script, because it is part of the member's domain constraint.
This is a useful teaching decision:
- the rule stays close to the state it protects;
- the object communicates responsibility clearly.
Example 2: Exporting lending state ๐ก
The library may need to expose lending state in multiple forms:
- a hash for internal use;
- JSON for an API;
- CSV for operations reporting.
That makes the exporter boundary realistic and shows the payoff from Topic 7.
Cheatsheet
Domain objects
Book: data about a lendable itemMember: borrowing rule and borrowed collectionLibrary: inventory and lending coordinationLibraryReport: representation/export boundary
Collaboration pattern
lib = Library.new
member = Member.new
lib.lend("isbn-1", member)ruby
Export boundary
class HashExporter
def export(state)
state
end
endruby
Design reminders โ๏ธ
- Put rules close to the object that owns them.
- Keep reporting/output logic outside the core domain workflow.
Study Guide
Topic purpose ๐ฏ
Use a small lending domain to consolidate the course. This topic is less about new syntax and more about whether the student can design a clean Ruby model.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.md. - Study
worked_examples.mdand identify where previous topics reappear. - Keep
cheatsheet.mdopen while readingexample.rbandbasic_exercise.rb. - Complete the library workflow exercise.
- Complete the reporting/export exercise.
What to notice ๐
- Domain rules should live in the object that owns them.
- Coordination code should remain separate from representation code.
- The exporter boundary makes the domain open to new outputs without distortion.
Reflection questions ๐ค
- Why is the borrow limit enforced in
Memberrather thanLibrary? - What earlier topics reappear in the report exporter?
- If this capstone grew larger, where would you expect the next abstraction boundary?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_09_mini_capstone
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.
class Book
attr_reader :title, :author, :isbn
def initialize(title:, author:, isbn:)
@title = title
@author = author
@isbn = isbn
end
end
class Member
MAX_BORROW = 3
attr_reader :borrowed
def initialize
@borrowed = []
end
def borrow(book)
raise ArgumentError, "borrow limit reached" if @borrowed.size >= MAX_BORROW
@borrowed << book
end
def return_book(book)
@borrowed.delete(book)
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_09_mini_capstone
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_09_mini_capstone_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.
class Library
def initialize
@books = []
@lent = {}
end
def add_book(book)
@books << book
end
def lend(isbn, member)
return false if @lent.key?(isbn)
book = @books.find { |b| b.isbn == isbn }
return false unless book
member.borrow(book)
@lent[isbn] = member
true
end
def return_book(isbn, member)
return false unless @lent[isbn] == member
book = @books.find { |b| b.isbn == isbn }
member.return_book(book)
@lent.delete(isbn)
true
end
def lending_state
@lent.transform_values { |member| member.object_id }
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_09_mini_capstone
#
# Academic purpose:
# - Consolidate the course by separating domain behavior from output behavior.
# - Show that even a small domain benefits from composition and duck typing.
#
# Real-world use case:
# - Library, inventory, and lending systems often need multiple reports for operators, APIs,
# and downstream systems.
# - Exporters are useful because the core lending model should not care whether the output is a
# hash, JSON document, CSV file, or some future integration payload.
# - This mirrors a common production design boundary.
#
# Why Ruby is beautiful here:
# - The domain model stays small and readable.
# - The report object depends only on a tiny exporter contract.
# - Earlier course ideas combine naturally rather than feeling isolated.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain how this file reuses ideas from composition and duck typing.
# - Use tests in tests/topic_09_mini_capstone_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can describe the architecture, not only the syntax.
class HashExporter
def export(state)
state
end
end
class LibraryReport
def initialize(library:, exporter:)
@library = library
@exporter = exporter
end
def call
@exporter.export(@library.lending_state)
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_09_mini_capstone
#
# Solution idea:
# - `Library` coordinates inventory and lending state.
# - Lending succeeds only if the book exists and is not already lent out.
# - Returning succeeds only when the same member currently holds the book.
# - `lending_state` exposes a simple reporting-friendly view of the current loans.
class Library
def initialize
@books = []
@lent = {}
end
def add_book(book)
@books << book
end
def lend(isbn, member)
return false if @lent.key?(isbn)
book = @books.find { |candidate| candidate.isbn == isbn }
return false unless book
member.borrow(book)
@lent[isbn] = member
true
end
def return_book(isbn, member)
return false unless @lent[isbn] == member
book = @books.find { |candidate| candidate.isbn == isbn }
member.return_book(book)
@lent.delete(isbn)
true
end
def lending_state
@lent.transform_values { |member| member.object_id }
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_09_mini_capstone
#
# Solution idea:
# - Keep reporting outside the core `Library` domain object.
# - The exporter is duck-typed: any object with `export(state)` can be used.
# - This keeps the domain model open to future output formats without changing the lending logic.
class HashExporter
def export(state)
state
end
end
class LibraryReport
def initialize(library:, exporter:)
@library = library
@exporter = exporter
end
def call
@exporter.export(@library.lending_state)
end
end
Ruby course source
# This spec is your learning companion for topic_09_mini_capstone.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_09_mini_capstone" do
describe "EXAMPLE purpose: understand the reference implementation" do
it "creates books and enforces member borrow limits" do
member = Member.new
3.times { |i| member.borrow(Book.new(title: "T#{i}", author: "A", isbn: i.to_s)) }
expect { member.borrow(Book.new(title: "Overflow", author: "A", isbn: "x")) }.to raise_error(ArgumentError, /limit/)
end
end
describe "BASIC EXERCISE purpose: implement the comparable task" do
it "lends and returns books through library" do
lib = Library.new
member = Member.new
book = Book.new(title: "Practical Ruby", author: "A", isbn: "isbn-1")
lib.add_book(book)
expect(lib.lend("isbn-1", member)).to eq(true)
expect(member.borrowed).to eq([book])
expect(lib.return_book("isbn-1", member)).to eq(true)
expect(member.borrowed).to eq([])
end
end
describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
it "exports lending state via duck-typed exporter" do
lib = Library.new
member = Member.new
book = Book.new(title: "Practical Ruby", author: "A", isbn: "isbn-1")
lib.add_book(book)
lib.lend("isbn-1", member)
report = LibraryReport.new(library: lib, exporter: HashExporter.new)
expect(report.call).to eq({ "isbn-1" => member.object_id })
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.