Use Cases

Object-Oriented Design with UML

Course 1 · Chapter 3 · From Requirements to Use Cases

Analysis starts with a question that has nothing to do with classes: who wants what from this system, and what has to happen for them to get it? A use case answers it for one goal, as a short story with a happy path and the ways it can go wrong. Use cases come before the domain model (chapter 4) because the concepts you need are the ones the stories mention, and they come before any class design because you cannot decide who is responsible for something nobody has described.

What was run, and what was not
The two use case diagrams were written in PlantUML 1.2026.8 and rendered here. The "Borrow a Book" use case description is turned into Python (3.14) with one test per flow and the output pasted; a second run shows a deliberate bug being caught. The library is invented: the 21-day loan, the limit of five and the rule that fines block borrowing are my assumptions, listed as open questions for a real librarian. Three things come from general knowledge and were not re-read for this chapter: that use cases were popularised by Ivar Jacobson; the goal levels and the step-numbering convention for alternative flows, which I attribute to Alistair Cockburn's Writing Effective Use Cases; and the exact UML meaning of include and extend.

The Words

TermMeaningLibrary example
ActorSomeone or something outside the system that interacts with it, in a role (a role, not a job title or a person)Member, Librarian, the card payment service
Primary actorThe actor whose goal the use case servesMember, for "Reserve a book"
Supporting actorAn actor the system calls on to reach the goalThe card payment service, for "Pay a fine"
GoalWhat the primary actor wants to achieve; the name of the use case"Borrow a book"
ScopeThe system being designed, treated as a black box; everything outside it is an actorThe library lending system, not the whole library
Use caseA goal-directed set of interactions between actors and the system, including failures along the way"Borrow a Book", with its flows
ScenarioOne path through a use caseBorrow succeeds; borrow refused because of fines

Two points that prevent most confusion. Roles, not people: a person can play both Member and Librarian (a librarian borrowing a book); the use case cares about the role. Scope decides who is an actor: inside the lending system the librarian is an actor, because the system is the software. If your scope were "the library as an organisation", the librarian would be inside the boundary. Always state the scope.

Finding Actors and Goals

A reliable method is to list actors first and then ask each one the same question: what do you want to get done with this system? Ask "what else?" until the answers dry up, then ask what happens on a schedule, with no person involved (a nightly job is an actor too). For the library:

ActorGoals
MemberSearch the catalogue; borrow a book; return a book; reserve a book; renew a loan; pay a fine
LibrarianBorrow and return books at the desk for a member; join a new member; add books to stock
Card payment service (supporting)Takes the payment for a fine
A nightly timer (exercise 2)Send overdue notices

Goals have a level. Cockburn's names (from memory) are cloud, kite, sea level, fish and clam. The useful one is sea level, or "user goal": something a person does in one sitting that is worth doing for its own sake, such as "Borrow a book". Above it ("Run the library") is too big to describe; below it ("Enter the card number") is a step, not a goal. A quick test: after finishing it, would the actor consider the job done, and could they walk away?

The Use Case Diagram

Library lending systemSearch the catalogueBorrow a bookReturn a bookReserve a bookRenew a loanPay a fineJoin the libraryAdd books to stockMemberLibrarianCard PaymentService«system»

PlantUML, rendered here. The rectangle is the scope; actors stand outside it; each line says "this actor takes part in this use case".

@startuml scale 1.4 left to right direction actor Member actor Librarian actor "Card Payment\nService" as Pay <<system>> rectangle "Library lending system" { usecase "Search the catalogue" as Search usecase "Borrow a book" as Borrow usecase "Return a book" as Return usecase "Reserve a book" as Reserve usecase "Renew a loan" as Renew usecase "Pay a fine" as PayFine usecase "Join the library" as Join usecase "Add books to stock" as Stock } Member --> Search Member --> Borrow Member --> Return Member --> Reserve Member --> Renew Member --> PayFine Librarian --> Borrow Librarian --> Return Librarian --> Join Librarian --> Stock PayFine --> Pay @enduml

That is the whole use case diagram, and it is useful for exactly one thing: showing the scope and the cast on one page, so a customer can say "you forgot that members can renew". Its limits are as important:

  • It says nothing about what happens. "Borrow a book" is an ellipse; whether a member with fines can borrow, and what the librarian sees if not, is in the description below, not the picture.
  • It has no order and no flow. Neither arrows nor position mean "first".
  • It does not scale. Past a dozen ellipses it becomes wallpaper. Group by actor or by area into several diagrams.
  • The arrowheads are a drawing convention (from memory of the UML notation, where the line between an actor and a use case is an association). Here the arrow runs from the primary actor to the use case, and from a use case to a supporting actor it needs.

Many teams keep only a list of use case names and put the effort into the descriptions. That is a fair choice.

The Use Case Description

The description is the real content. A common layout (Cockburn's, as I remember it) has these parts:

PartWhat goes in it
Name, scope, level, primary actorName it as a verb phrase for the goal; say what the system is and who wants this
Stakeholders and interestsEveryone who cares about the outcome, even if they never touch the system, and what they want (this finds rules that the actors would not mention)
PreconditionWhat must already be true for the use case to start (not a check the use case performs)
TriggerThe event that starts it
Main success scenarioNumbered steps of the simplest path in which everything goes right
ExtensionsWhat can go differently, attached to the step where it happens (3a, 3b...), each ending with either "use case ends" or "resume at step N"
GuaranteesSuccess guarantee: what is true afterwards if it worked. Minimal guarantee: what is true at least, however it ends

The library's "Borrow a Book", written out in full:

USE CASE: Borrow a Book Scope: Library lending system (black box) Goal level: User goal ("sea level") Primary actor: Librarian, acting for a Member at the desk Stakeholders: Member (wants the book and a clear due date) Library (wants every loan recorded, and books returned) Precondition: The Librarian is signed in. Trigger: A Member hands over a book and their library card. Success guarantee: A loan is recorded, the copy is marked on loan, and the due date is known. Minimal guarantee: If the loan is refused, nothing has changed, and the Librarian knows why. MAIN SUCCESS SCENARIO 1. The Member gives the Librarian the book and the card. 2. The Librarian enters the card number and the book's ISBN. 3. The system checks the membership is valid. 4. The system checks the Member has no unpaid fines. 5. The system checks the Member is under the loan limit (5). 6. The system checks this copy can be lent to this Member. 7. The system records the loan, due in 21 days. 8. The system tells the Librarian the due date. 9. The Librarian gives the book to the Member and tells them the date. EXTENSIONS 2a. The catalogue has no book with that ISBN: 2a1. The system refuses, saying so. Use case ends. 3a. The card is unknown, or the membership has expired: 3a1. The system refuses, saying the membership is not valid. Use case ends. 4a. The Member has unpaid fines: 4a1. The system refuses and offers "Pay a fine". Use case ends (or continues from step 4 if the fine is paid now). 5a. The Member already has 5 loans: 5a1. The system refuses, saying the limit is reached. Use case ends. 6a. The copy is already on loan: 6a1. The system refuses and offers "Reserve a book". Use case ends. 6b. The copy is being held for a different Member: 6b1. The system refuses, saying it is held. Use case ends. 6c. The copy is being held for THIS Member: 6c1. The system continues at step 7, and the hold is fulfilled (cleared). OPEN QUESTIONS FOR THE LIBRARIAN - Is 21 days right? Is 5 loans right? Do all books lend for the same time? - Should a Member with a small fine really be refused (4a)?

How It Reads

  • The steps are about the actor and the system, not the screen. "The Librarian enters the card number and the ISBN" does not say whether that is a form, a scanner or a command line; deciding that is design.
  • Each check is its own step (3 to 6), because each can fail differently, and each failure is an extension with the same number. This is where the value is: the main scenario is short and obvious, and the extensions list contains the decisions a developer would otherwise make silently.
  • Extensions say how the use case ends, so no flow is left hanging. 4a even links to another use case (Pay a fine) and says whether to resume.
  • The minimal guarantee is a promise about failure: a refused loan changes nothing. That sentence is cheap to write and expensive to forget, because a half-recorded loan is a bug nobody notices until the data is wrong.
  • The open questions are part of the document. A use case written by someone who does not run the library will contain guesses; marking them is honest and tells you what to ask.

From Description to Check

A use case that cannot be turned into a test is probably vague. Here the description above became a Python function, with each numbered step and each extension as a comment in the code at the place it happens, and one test per flow. This is not the design you will end up with (who owns these checks is chapter 6's question); it is a way to make the description execute.

"""The 'Borrow a Book' use case as code. Each numbered step and each extension of the description appears as a comment.""" from dataclasses import dataclass, field from datetime import date, timedelta LOAN_DAYS = 21 MAX_LOANS = 5 class Refusal(Exception): """A use case extension that ends in failure. The message is what the Librarian reads out.""" @dataclass class Member: card: str expires_on: date fines_pence: int = 0 loans: list = field(default_factory=list) @dataclass class Copy: isbn: str on_loan: bool = False held_for: str | None = None # card number of the member a reservation is held for @dataclass class Loan: card: str isbn: str due_on: date class Library: def __init__(self, members, copies): self.members = {m.card: m for m in members} self.copies = {c.isbn: c for c in copies} def borrow(self, card: str, isbn: str, today: date) -> Loan: member = self.members.get(card) # 2. The Librarian enters the card number and the book's ISBN. if isbn not in self.copies: # 2a. the catalogue has no such book raise Refusal("no such book in the catalogue") # 3. The system checks the membership is valid. if member is None or member.expires_on < today: # 3a. unknown or expired raise Refusal("membership is not valid") # 4. The system checks the member has no unpaid fines. if member.fines_pence > 0: # 4a. raise Refusal("unpaid fines: offer 'Pay a fine'") # 5. The system checks the member is under the loan limit. if len(member.loans) >= MAX_LOANS: # 5a. raise Refusal("loan limit reached") # 6. The system checks the copy can be lent to this member. copy = self.copies[isbn] if copy.on_loan: # 6a. raise Refusal("on loan: offer 'Reserve a book'") if copy.held_for not in (None, card): # 6b. held for someone else raise Refusal("held for another member") # 7. The system records the loan and the due date. (6c: held for THIS member: the hold is fulfilled) loan = Loan(card, isbn, today + timedelta(days=LOAN_DAYS)) copy.on_loan, copy.held_for = True, None member.loans.append(loan) # 8. The system tells the Librarian the due date. return loan
import copy as cp import unittest from datetime import date from borrow_usecase import MAX_LOANS, Copy, Library, Loan, Member, Refusal TODAY = date(2026, 10, 10) def library(**overrides): member = Member(**{"card": "M1", "expires_on": date(2027, 1, 1), **overrides.get("member", {})}) copies = [Copy("111", **overrides.get("c111", {})), Copy("222")] return Library([member], copies) class MainFlow(unittest.TestCase): def test_success_postconditions(self): lib = library() loan = lib.borrow("M1", "111", TODAY) self.assertEqual(loan.due_on, date(2026, 10, 31)) # due date set self.assertTrue(lib.copies["111"].on_loan) # copy no longer available self.assertEqual(lib.members["M1"].loans, [loan]) # loan recorded class Extensions(unittest.TestCase): def refused(self, lib, card="M1", isbn="111", expect=""): before = cp.deepcopy((lib.members, lib.copies)) with self.assertRaises(Refusal) as ctx: lib.borrow(card, isbn, TODAY) self.assertIn(expect, str(ctx.exception)) self.assertEqual((lib.members, lib.copies), before) # minimal guarantee: a refusal changes nothing def test_2a_unknown_book(self): self.refused(library(), isbn="999", expect="catalogue") def test_3a_unknown_card(self): self.refused(library(), card="NOPE", expect="membership") def test_3a_expired_membership(self): self.refused(library(member={"expires_on": date(2026, 10, 9)}), expect="membership") def test_4a_unpaid_fines(self): self.refused(library(member={"fines_pence": 50}), expect="fines") def test_5a_loan_limit(self): loans = [Loan("M1", str(i), TODAY) for i in range(MAX_LOANS)] self.refused(library(member={"loans": loans}), expect="limit") def test_6a_on_loan(self): self.refused(library(c111={"on_loan": True}), expect="Reserve") def test_6b_held_for_someone_else(self): self.refused(library(c111={"held_for": "M2"}), expect="another member") def test_6c_held_for_this_member_succeeds_and_clears_the_hold(self): lib = library(c111={"held_for": "M1"}) lib.borrow("M1", "111", TODAY) self.assertIsNone(lib.copies["111"].held_for) def test_membership_expiring_today_is_still_valid(self): lib = library(member={"expires_on": TODAY}) self.assertEqual(lib.borrow("M1", "111", TODAY).isbn, "111") if __name__ == "__main__": unittest.main(verbosity=2)
test_2a_unknown_book (test_borrow_usecase.Extensions.test_2a_unknown_book) ... ok test_3a_expired_membership (test_borrow_usecase.Extensions.test_3a_expired_membership) ... ok test_3a_unknown_card (test_borrow_usecase.Extensions.test_3a_unknown_card) ... ok test_4a_unpaid_fines (test_borrow_usecase.Extensions.test_4a_unpaid_fines) ... ok test_5a_loan_limit (test_borrow_usecase.Extensions.test_5a_loan_limit) ... ok test_6a_on_loan (test_borrow_usecase.Extensions.test_6a_on_loan) ... ok test_6b_held_for_someone_else (test_borrow_usecase.Extensions.test_6b_held_for_someone_else) ... ok test_6c_held_for_this_member_succeeds_and_clears_the_hold (test_borrow_usecase.Extensions.test_6c_held_for_this_member_succeeds_and_clears_the_hold) ... ok test_membership_expiring_today_is_still_valid (test_borrow_usecase.Extensions.test_membership_expiring_today_is_still_valid) ... ok test_success_postconditions (test_borrow_usecase.MainFlow.test_success_postconditions) ... ok Ran 10 tests in 0.001s OK

Two observations from doing this. First, writing the code found a missing extension. My first version of the description had no extension 2a: the code looked the ISBN up in the catalogue, and a book that was not there raised a Python KeyError, not a refusal the Librarian could read:

--- before extension 2a was written KeyError: '999-not-in-catalogue' <- not a Refusal: the description never said what happens here --- after Refusal: no such book in the catalogue <- the Librarian can read this out

The description was incomplete; the code was merely the first thing to ask the awkward question. I added extension 2a, the test for it, and ran again. Second, I tested the tests. The minimal guarantee ("a refusal changes nothing") is checked by comparing a deep copy of the data before and after every refusal. To see whether that test could fail, I ran the suite against a version of borrow with a deliberate bug that changes the copy's state before the checks that may refuse:

FAIL: test_3a_expired_membership (test_borrow_usecase.Extensions.test_3a_expired_membership) FAIL: test_3a_unknown_card (test_borrow_usecase.Extensions.test_3a_unknown_card) FAIL: test_4a_unpaid_fines (test_borrow_usecase.Extensions.test_4a_unpaid_fines) FAIL: test_5a_loan_limit (test_borrow_usecase.Extensions.test_5a_loan_limit) FAIL: test_6a_on_loan (test_borrow_usecase.Extensions.test_6a_on_loan) FAIL: test_6b_held_for_someone_else (test_borrow_usecase.Extensions.test_6b_held_for_someone_else) mutant run: tests failed: 6 of 10

Six tests failed, so the guarantee is really being checked. (Test code that never fails on broken code proves nothing; Build a Chatbot: From API Call to Deployed Bot 11 made the same point with simulations.) Note what the tests do not establish: that 21 days and a limit of five are right. Only the librarian can say.

Use Cases and User Stories

The Software Development Lifecycle 5 covers requirements and user stories; this section only relates the two. A story such as "As a member, I want to borrow a book so that I can read it at home" is a short reminder of a need, meant to start a conversation. A use case is what that conversation produces when the goal has real flows: the steps, the checks and the failures. They are not rivals.

User storyUse case
SizeOne or two sentencesOne to two pages
StrengthPlanning, prioritising, small slices of valueComplete flows, rules and failures for one goal
WeaknessDetail lives in conversations and tests that can be lostHeavier to write and to keep current
Use whenThe goal is simple or the team talks dailyA goal has many rules and failure modes, or the people deciding are not in the room

A practical way to use both: let the use case supply the stories. "Borrow a book" gives one story for the main scenario and one for each extension that is worth building separately ("As a member with fines, I want to be told why I was refused and how to fix it"); the extensions double as acceptance criteria.

Include and Extend, and Why to Be Sparing

Use cases can relate to each other in two ways (from memory of the UML standard and its usual explanations). Include: the base use case always performs the included one, as a step; use it to share a piece of behaviour that several use cases need. Extend: the extending use case optionally adds behaviour to the base at a named point, under a condition; the arrow runs from the extension to the base. A small example, rendered:

Library lending systemBorrow a bookRenew a loanCheck eligibilityReturn a bookPay a fineMember«include»«include»«extend»

Borrowing and renewing both include "Check eligibility". "Pay a fine" can extend "Return a book" when the return is late.

@startuml scale 1.4 left to right direction actor Member rectangle "Library lending system" { usecase "Borrow a book" as Borrow usecase "Renew a loan" as Renew usecase "Check eligibility" as Check usecase "Return a book" as Return usecase "Pay a fine" as PayFine } Member --> Borrow Member --> Renew Member --> Return Borrow ..> Check : <<include>> Renew ..> Check : <<include>> PayFine ..> Return : <<extend>> @enduml

Read the arrows: an include arrow points to the included use case; an extend arrow points to the base. People draw them backwards constantly. And a warning: the temptation is to overuse both. Splitting use cases into include-pieces turns the description into a functional decomposition (a flowchart in disguise), and the readers lose the story. Reach for them only when the sharing is real (the same several steps in several use cases) or the optional behaviour genuinely belongs elsewhere. Writing the extension inline ("4a. The Member has unpaid fines...") is usually clearer. In the Borrow description above I did exactly that, and referred to Pay a fine by name instead of drawing an extend relationship.

Common Mistakes

MistakeExampleBetter
A use case that is a screen or a data operation"Manage books" (create, read, update, delete)Name the goals: "Add books to stock", "Withdraw a damaged book"
A step dressed as a use case"Enter card number"Make it a step inside "Borrow a book"
The user interface in the flow"The Librarian clicks the green Borrow button""The Librarian asks to borrow the book" (the interface is a later decision)
The system as an actor, or an actor inside the system"The Database" as an actorInside the scope it is design. An actor is outside it: a payment service, a timer
Only the happy pathA one-page description with no extensionsWalk each step and ask "what if this fails?"; write it down
Too many levels at onceA diagram mixing "Run the library" and "Scan a barcode"Keep to sea level; mention the others in prose
Include and extend everywhereTwenty ellipses joined by dotted arrowsInline the extensions; share only what is truly shared
Never taking it to the people who knowA design finished without asking the librarianRead the description aloud to them; it is written in their language, which is its point
Mistakes while preparing this chapter
  • I identified a physical book by its ISBN. The description and the code say "the book's ISBN" and look the copy up by it. That conflates two things: an ISBN identifies a title, and a library may own several physical copies of one title. Chapter 4's domain model exposes it (its demonstration shows two copies of one ISBN collapsing to one in this chapter's code, so a second member is refused a copy that is on the shelf). The fix, not made here, is to say "copy barcode" in the steps and add a Title/Copy distinction. Takeaway: a use case written before the domain model is a draft of its vocabulary; expect the model to correct it.
  • A broken test helper. My first run of the use case tests failed two tests with TypeError: Member.__init__() got multiple values for argument 'expires_on'. What went wrong: the test helper passed expires_on by position and then again through the overrides. How to avoid it: build test fixtures from a dictionary of defaults that overrides replace, never from a positional argument plus overrides.
  • An incomplete use case, found late. The description had no extension for an unknown ISBN until the code raised KeyError (shown above). The takeaway is the practice, not the apology: turn each description into a runnable check as early as is cheap, because code is the strictest reader a requirement will ever have.
  • A demonstration that printed the wrong thing. My first "gap" demo printed "not a Refusal" on every line, even after the fix, because the message was written as a constant instead of depending on the exception type. It said the opposite of what happened. Takeaway: make a demonstration's commentary depend on what it observes.
  • A command that did not do what I thought. A sed command to remove a label from a diagram silently matched nothing (the file held a literal backslash-n, and my pattern assumed a newline), and the render script still said "rendered". I only noticed because I looked at the picture. Takeaway: after an automated edit, check the file or the picture, not just the exit status.

Hands-On Exercises

Exercise 1

Write the use case description for "Return a Book" in the full layout: scope, level, primary actor, stakeholders, precondition, trigger, a main success scenario, extensions (no open loan for the copy, an overdue book with a fine, another member waiting in the reservation queue, a damaged book), and the two guarantees. List any rule you had to assume.

📄 View solution
Exercise 2

In PlantUML, draw a use case diagram for the library with Member, Librarian and a nightly timer as actors (the timer triggers "Send overdue notice"). Draw the scope boundary. Say whether the Member is a primary or supporting actor for the notice, and why.

📄 View solution
Exercise 3

Turn your "Return a Book" description into Python with a test for the main scenario and each extension, and run it. Look for a case your description did not decide, and write down the question for the librarian.

📄 View solution

Chapter 3 Quick Reference

  • Actor = a role outside the system; scope = the system as a black box; goal = what the primary actor wants; state the scope
  • Find use cases by asking each actor "what do you want to get done?" and "what else?"; include scheduled jobs; aim for sea-level goals
  • The use case diagram shows scope and cast on one page and nothing more; keep it small; many teams use a list instead
  • The description is the content: precondition, trigger, numbered main scenario, extensions numbered by the step they attach to, each ending or resuming; success and minimal guarantees
  • One check per step, so each failure gets its own extension; keep the user interface out of the steps
  • Make the description executable early: it finds missing extensions, and test the tests by breaking the code on purpose
  • Story = reminder for a conversation; use case = the detail it produces; extensions double as acceptance criteria
  • include arrow points to the included use case (always performed); extend arrow points to the base (optional, conditional); use sparingly, inline extensions first
Coming next
Object-Oriented Design with UML 4 (The Domain Model): finding the concepts that the use cases mention (noun and verb analysis and its weaknesses), attributes, associations and multiplicity, a glossary, and the library's domain model as a class diagram.