Sequence Diagrams

Object-Oriented Design with UML

Course 1 ยท Chapter 7 ยท Sequence Diagrams

A class diagram shows who exists; it does not show how they cooperate. Chapter 6 gave the library's classes their responsibilities, but a reader still cannot see what happens when a librarian borrows a book: who is called first, who answers, and what happens when a rule says no. A sequence diagram shows one scenario as a conversation between objects, with time running downwards. It is the diagram that turns a use case into operations on the classes.

What was run, and what was not
Every diagram was rendered here with Mermaid 12.1.0, except the one with a ref fragment, which Mermaid cannot draw (I tried: parse error) and which PlantUML 1.2026.8 drew. The Python (3.14) is chapter 6's version B, run for real. New in this chapter: a call tracer I wrote that records what the code actually does and draws it as a sequence diagram, a conformance check that compares a hand-drawn diagram with the code, and a check of the operations a sequence diagram needs against the class diagram. They are small scripts, shown in full, with their limits stated. Not done: communication, timing and interaction-overview diagrams (the paragraph on communication diagrams is from general knowledge, with nothing drawn or run); any tool that generates sequence diagrams from other languages.

Reading the Notation

LoanNotifierMemberLoanDeskLoanNotifierMemberLoanDeskasynchronous: the desk does not waitLibrarianborrow(card, barcode, today)1borrow(copy, today)2check_can_borrow(today)3create Loan(...)4Loan5loanRecorded(loan)6Loan7Librarian

Mermaid. The numbers come from autonumber; the Loan box appears partway down because it is created during the scenario.

sequenceDiagram autonumber actor Librarian participant LoanDesk participant Member participant Notifier Librarian->>+LoanDesk: borrow(card, barcode, today) LoanDesk->>+Member: borrow(copy, today) Member->>Member: check_can_borrow(today) create participant Loan Member->>Loan: create Loan(...) Member-->>-LoanDesk: Loan LoanDesk-)Notifier: loanRecorded(loan) Note right of Notifier: asynchronous: the desk does not wait LoanDesk-->>-Librarian: Loan
ElementLooks likeMermaidMeans
Participant, lifelineA box with a dashed or solid line downparticipant Member, actor LibrarianOne object (or class, or person) taking part; time passes down the line. Name an object's role or class, not a method
Synchronous messageSolid line, filled headA->>B: op(args)A calls an operation of B and waits
ReturnDashed line, open headB-->>A: valueThe result comes back. Optional if obvious; useful when the value matters
Asynchronous messageSolid line, open headA-)B: eventA sends and carries on without waiting (a notification, a queue)
ActivationA thin bar on a lifelineA->>+B and B-->>-AThe period when B is executing because of the call
Self messageAn arrow looping backM->>M: check(today)An object calling its own operation
CreationA box appearing mid-diagramcreate participant Loan, then a message to itThe scenario makes a new object
FailureLine ending in a crossA--xB: RefusalAn exception or refusal passed back (a common convention; not the only one)
NoteA yellow labelNote right of N: textA comment

From Use Case to Sequence Diagram

In Object-Oriented Design with UML 3 the Borrow use case treated the system as a black box: "the system checks the membership...". Chapter 6 opened the box and assigned those checks to objects. A sequence diagram is where you show which object does each step. The method:

  1. Pick one scenario: the main success scenario, or a single extension. One diagram, one path (fragments, below, fold a few alternatives in).
  2. Name the participants: the actor, then the controller that receives the request (LoanDesk), then the objects the responsibilities landed on.
  3. Turn each step into a message to the object that should do it, using chapter 6's reasoning (Expert, Creator, Controller). Show a return when the value is used later.
  4. Read the diagram out loud as the use case, and check no step was lost.
Use case step (chapter 3)Message in the diagram below
2. The Librarian enters the card number and ISBN (copy barcode, after chapter 4's correction)Librarian ->> LoanDesk: borrow(card, barcode, today)
(finding the member and the copy: part of how the system answers)LoanDesk ->> Catalogue: member(card), copy(barcode)
3, 4, 5. Check membership, fines, loan limitMember ->> Member: check_can_borrow(today)
6. Check this copy can be lentMember ->> Copy: check_can_be_lent()
7. Record the loan, due in 21 daysMember ->> Loan: create Loan(...)
8. Tell the Librarian the due dateThe two Loan returns, back to the Librarian
LoanCopyMemberCatalogueLoanDeskLoanCopyMemberCatalogueLoanDeskLibrarianborrow(card, barcode, today)member(card)Membercopy(barcode)Copyborrow(copy, today)check_can_borrow(today)check_can_be_lent()create Loan(...)LoanLoanLibrarian

"Borrow a book", main success scenario, for chapter 6's design. Drawn by hand from chapter 6's design. (Full disclosure: I had already seen the tracer's output for this scenario when I wrote it, so it is not a blind test; exercise 1 is the blind one.)

sequenceDiagram actor Librarian participant LoanDesk participant Catalogue participant Member participant Copy participant Loan Librarian->>LoanDesk: borrow(card, barcode, today) LoanDesk->>Catalogue: member(card) Catalogue-->>LoanDesk: Member LoanDesk->>Catalogue: copy(barcode) Catalogue-->>LoanDesk: Copy LoanDesk->>Member: borrow(copy, today) Member->>Member: check_can_borrow(today) Member->>Copy: check_can_be_lent() Member->>Loan: create Loan(...) Member-->>LoanDesk: Loan LoanDesk-->>Librarian: Loan

Alternatives and Failure: Fragments

A fragment is a labelled box around some messages with a condition, so one diagram can hold several paths. The ones you will use:

FragmentMeaningMermaid
altOne of several branches, chosen by a condition (if / else)alt ... else ... end
optHappens only if the condition holds (if with no else)opt ... end
loopRepeatedloop ... end
parBranches happen in parallelpar ... and ... end
ref"See another diagram here": a reference, to keep one diagram smallNot supported (parse error when tried); PlantUML has it
CopyMemberLoanDeskCopyMemberLoanDeskalt[copy is free][copy already on loan]alt[member may borrow][expired, fines, or at thelimit]Librarianborrow(card, barcode, today)borrow(copy, today)check_can_borrow(today)check_can_be_lent()LoanLoan (due date)Refusal "already on loan"Refusal (reason)Loan, or the Refusal passed onLibrarian

The borrow scenario with its refusals folded in: the first alt separates "member may borrow" from the failures the member's own checks raise, the nested one separates a free copy from one already on loan.

sequenceDiagram actor Librarian participant LoanDesk participant Member participant Copy Librarian->>+LoanDesk: borrow(card, barcode, today) LoanDesk->>+Member: borrow(copy, today) Member->>Member: check_can_borrow(today) alt member may borrow Member->>Copy: check_can_be_lent() alt copy is free Member-->>LoanDesk: Loan LoanDesk-->>Librarian: Loan (due date) else copy already on loan Member--xLoanDesk: Refusal "already on loan" end else expired, fines, or at the limit Member--xLoanDesk: Refusal (reason) end deactivate Member LoanDesk-->>-Librarian: Loan, or the Refusal passed on

Look at who throws. The refusal for an unavailable copy comes from Copy, not Member; the diagram draws it from the member's call because the member passes the exception up. I simplified it, and a stricter reader would add a second arrow. That is the usual trade: a diagram is a model, and every line you add makes it harder to read.

Loop, Opt and Par

NotifierMemberLoanDeskNotifierMemberLoanDeskloop[for each book on the desk]opt[the member asked for areceipt]par[by email][by text message]Librarianborrow(card, barcode, today)Loanreceipt(card)Receiptemail(member, loans)sms(member, loans)Librarian

A desk session: loop over books, optional receipt, then two notifications sent in parallel (asynchronously).

sequenceDiagram actor Librarian participant LoanDesk participant Member participant Notifier loop for each book on the desk Librarian->>LoanDesk: borrow(card, barcode, today) LoanDesk-->>Librarian: Loan end opt the member asked for a receipt Librarian->>LoanDesk: receipt(card) LoanDesk-->>Librarian: Receipt end par by email LoanDesk-)Notifier: email(member, loans) and by text message LoanDesk-)Notifier: sms(member, loans) end

And ref, which only PlantUML draws. The box stands for another diagram, so this one stays short:

LibrarianLoanDeskMemberLibrarianLoanDeskMemberLibrarianLoanDeskMemberreturn_book(barcode, today)reffind the open loan(see "Find a loan" diagram)give_back(loan, copy, today)finefine

PlantUML. The ref box names a sub-scenario drawn elsewhere.

@startuml scale 1.4 actor Librarian participant LoanDesk participant Member Librarian -> LoanDesk : return_book(barcode, today) ref over LoanDesk, Member : find the open loan\n(see "Find a loan" diagram) LoanDesk -> Member : give_back(loan, copy, today) Member --> LoanDesk : fine LoanDesk --> Librarian : fine @enduml

From Sequence Diagram to Operations

A message is a promise that the receiver has that operation. So reading a sequence diagram gives you the operations for the class diagram: LoanDesk ->> Member: borrow(copy, today) means Member needs borrow(copy, today). This is the link between the two diagrams, and it can be checked mechanically. The script reads the Mermaid sources, lists each (receiver, operation), and compares with the operations on chapter 6's class diagram for the same design (the second half of its output):

"""Check the hand-drawn sequence diagram against (1) what the code really does and (2) the class diagram's operations.""" import pathlib import re D = pathlib.Path(__file__).parent / "diagrams" def calls(path: pathlib.Path) -> set[tuple[str, str, str]]: """Only the calls (solid arrows ->> and async -)), as (sender, receiver, operation name).""" out = set() for line in path.read_text(encoding="utf8").splitlines(): m = re.match(r"^\s*(\w+)(?:->>|-\))(\w+):\s*(.+)$", line) if m: s, r, label = m.groups() op = "create" if label.startswith("create ") else label.split("(")[0] out.add((s, r, op)) return out def class_operations(path: pathlib.Path) -> dict[str, set[str]]: ops, current = {}, None for line in path.read_text(encoding="utf8").splitlines(): c = re.match(r"\s*class (\w+)", line) if c: current = c.group(1) ops.setdefault(current, set()) continue o = re.match(r"\s*\+(\w+)\(.*\)", line) if o and current: ops[current].add(o.group(1)) return ops if __name__ == "__main__": drawn, traced = calls(D / "borrow.drawn.mmd"), calls(D / "trace_borrow_ok.mmd") print("1. hand-drawn 'borrow' versus what the code did (calls only)") print(f" drawn: {len(drawn)} distinct calls | traced: {len(traced)} distinct calls") for c in sorted(drawn - traced): print(f" drawn but NOT in the trace : {c[0]} -> {c[1]}: {c[2]}") for c in sorted(traced - drawn): print(f" in the trace, NOT drawn : {c[0]} -> {c[1]}: {c[2]}") print() print("2. operations the sequence diagrams ask for, against the class diagram of chapter 6 (version B)") ops = class_operations(D / "version_b.class.mmd") for name in ("borrow.drawn.mmd", "session.fragments.mmd"): missing = sorted((r, op) for (_, r, op) in calls(D / name) if op != "create" and r in ops and op not in ops[r]) unknown = sorted({r for (_, r, op) in calls(D / name)} - set(ops) - {"Librarian"}) print(f" {name}") print(f" operations not on the receiving class in the class diagram: {missing or 'none'}") print(f" receivers that are not classes in the class diagram : {unknown or 'none'}")
1. hand-drawn 'borrow' versus what the code did (calls only) drawn: 7 distinct calls | traced: 10 distinct calls in the trace, NOT drawn : Member -> Loan: is_open in the trace, NOT drawn : Member -> Member: open_loans in the trace, NOT drawn : Member -> Standard: loan_limit 2. operations the sequence diagrams ask for, against the class diagram of chapter 6 (version B) borrow.drawn.mmd operations not on the receiving class in the class diagram: none receivers that are not classes in the class diagram : none session.fragments.mmd operations not on the receiving class in the class diagram: [('LoanDesk', 'receipt')] receivers that are not classes in the class diagram : ['Notifier']

Read part 2. The borrow diagram asks for nothing the class diagram lacks. The desk session diagram asks for LoanDesk.receipt and for a Notifier that is not in the class diagram at all. Neither is a mistake yet; each is a decision waiting to be made, found by comparing two drawings. Either the class diagram gains a Notifier and a receipt operation, or the sequence diagram is showing something out of scope. Exercise 2 repeats this for a new diagram.

Does the Drawing Match the Code?

Part 1 of that output compares the drawn borrow diagram with a trace of the real code. The tracer (full source below) uses Python's sys.settrace to record each call into one of the traced classes, who made it (the self of the calling frame), and what came back, then writes a Mermaid diagram. It is a small tool and it has limits: it sees only the classes in one file, draws the outermost caller as an actor, and attributes a call to the caller's class, not to the particular object.

"""Record what really happens when a function runs, and draw it as a Mermaid sequence diagram. Uses sys.settrace. Only frames in the traced module's classes are recorded. A call is drawn from the object whose method made it (the 'self' of the calling frame) to the object whose method was entered. The outermost caller is drawn as an actor (the script). """ import sys from dataclasses import dataclass @dataclass class Event: kind: str # call | return | raise caller: str callee: str label: str depth: int = 0 # how deep in the call stack the callee is (1 = called by the actor) def trace(module_file: str, func, *args, actor: str = "Librarian"): events: list[Event] = [] stack: list[str] = [actor] # class names of the active frames we care about def owner(frame) -> str | None: obj = frame.f_locals.get("self") if obj is not None: return type(obj).__name__ # a dataclass-generated __init__ or a plain function: the code object's qualified name starts with the class qual = frame.f_code.co_qualname return qual.split(".")[0] if "." in qual and not qual.startswith("<") else None def global_tracer(frame, event, arg): if event != "call": return None self_obj = frame.f_locals.get("self") from_module = frame.f_code.co_filename.endswith(module_file) # a dataclass writes __init__ with exec(), so its code object has no file name: recognise it by the class's module generated_init = frame.f_code.co_name == "__init__" and self_obj is not None and type(self_obj).__module__ == module_file[:-3] if not (from_module or generated_init): return None who = owner(frame) if who is None: return None name = frame.f_code.co_name argnames = frame.f_code.co_varnames[1: frame.f_code.co_argcount] if frame.f_locals.get("self") is not None else frame.f_code.co_varnames[: frame.f_code.co_argcount] # for a dataclass __init__, 'self' is also first if name == "__init__": argnames = frame.f_code.co_varnames[1: frame.f_code.co_argcount] label = f"create {who}({', '.join(argnames)})" else: label = f"{name}({', '.join(argnames)})" events.append(Event("call", stack[-1], who, label, len(stack))) stack.append(who) def local_tracer(frame, event, arg): if event == "exception": local_tracer.exc = arg[1] elif event == "return": caller = stack[-2] if len(stack) > 1 else actor exc = getattr(local_tracer, "exc", None) if arg is None and exc is not None: events.append(Event("raise", who, caller, f"{type(exc).__name__}: {exc}", len(stack) - 1)) elif arg is not None and name != "__init__": events.append(Event("return", who, caller, type(arg).__name__, len(stack) - 1)) stack.pop() return local_tracer local_tracer.exc = None return local_tracer sys.settrace(global_tracer) try: result = func(*args) error = None except Exception as e: # the traced code's own refusals result, error = None, e finally: sys.settrace(None) return events, result, error def to_mermaid(events: list[Event], actor: str = "Librarian", skip: set[str] | None = None) -> str: skip = skip or set() order = [actor] for e in events: for who in (e.caller, e.callee): if who not in order: order.append(who) lines = ["sequenceDiagram"] for who in order: lines.append(f" actor {who}" if who == actor else f" participant {who}") for e in events: if any(s in e.label for s in skip): continue if e.kind == "call": lines.append(f" {e.caller}->>{e.callee}: {e.label}") elif e.kind == "return": lines.append(f" {e.caller}-->>{e.callee}: {e.label}") else: lines.append(f" {e.caller}--x{e.callee}: {e.label}") return "\n".join(lines) + "\n"

The refused-borrow scenario (the member has unpaid fines), as the code actually ran it:

MemberCatalogueLoanDeskMemberCatalogueLoanDeskLibrarianborrow(card, barcode, today)member(card)Membercopy(barcode)Copyborrow(copy, today)check_can_borrow(today)Refusal: unpaid finesRefusal: unpaid finesRefusal: unpaid finesLibrarian

Generated from a real run of chapter 6's library_b.py; I wrote none of these arrows. The refusal travels back up the chain: from the member, to the desk, to the librarian.

The comparison in part 1 of the output found three calls in the trace that the hand-drawn diagram omits: Member -> Member.open_loans, Member -> Loan.is_open and Member -> Standard.loan_limit. The first two are helpers, and leaving them out is a legitimate choice. The third deserves a second look: it is the call to the member's kind, the polymorphic collaboration chapter 6 built (the loan limit differs by kind of member). My drawing left out the one call that carries the design's main idea. Detail is a choice you make per audience; just notice what you are choosing to hide.

The Diagram as a Test

Once the hidden calls are agreed, the comparison can be a test: do the code's calls, filtered to the diagram's level of detail, equal the diagram's calls in the same order? The check below runs it on the real code, and on a version of the code where I made a plausible refactoring (looking the copy up before the member):

"""A sequence diagram as a test: does the code make the calls the diagram shows, in the order it shows them? The drawn diagram is deliberately less detailed than the code, so the trace is filtered to the same level of detail first: helper calls the diagram chooses not to show (open_loans, is_open, and the call to the member kind) are left out. """ import difflib import pathlib import re import types from datetime import date from tracetool import to_mermaid, trace HERE = pathlib.Path(__file__).parent HIDDEN = {"open_loans", "is_open", "loan_limit"} # abstraction chosen by the diagram's author def ordered_calls(text: str) -> list[str]: out = [] for line in text.splitlines(): m = re.match(r"^\s*(\w+)->>(\w+):\s*(.+)$", line) if m: sender, receiver, label = m.groups() op = "create" if label.startswith("create ") else label.split("(")[0] if op not in HIDDEN: out.append(f"{sender} -> {receiver}.{op}") return out def load(source: str): """Run library_b's source as a module with the same file name, so the tracer treats its classes as the traced ones.""" module = types.ModuleType("library_b") exec(compile(source, str(HERE / "library_b.py"), "exec"), module.__dict__) return module def check(name: str, module) -> bool: desk = module.LoanDesk([module.Member("M1", date(2027, 1, 1), module.KINDS["standard"])], [module.Copy("B0")]) events, result, error = trace("library_b.py", desk.borrow, "M1", "B0", date(2026, 10, 10)) actual = ordered_calls(to_mermaid(events)) drawn = ordered_calls((HERE / "diagrams" / "borrow.drawn.mmd").read_text(encoding="utf8")) if actual == drawn: print(f"{name}: the code makes the {len(drawn)} calls the diagram shows, in the same order") return True print(f"{name}: DOES NOT MATCH the diagram ('-' drawn, '+' actual)") for line in difflib.unified_diff(drawn, actual, lineterm="", n=0): if not line.startswith(("---", "+++", "@@")): print(" ", line) return False if __name__ == "__main__": source = (HERE / "library_b.py").read_text(encoding="utf8") check("code as written ", load(source)) # a plausible refactoring: look the copy up before the member old = " return self.catalogue.member(card).borrow(self.catalogue.copy(barcode), today)\n" new = " copy = self.catalogue.copy(barcode)\n return self.catalogue.member(card).borrow(copy, today)\n" assert source.count(old) == 1 check("copy looked up first", load(source.replace(old, new)))
code as written : the code makes the 7 calls the diagram shows, in the same order copy looked up first: DOES NOT MATCH the diagram ('-' drawn, '+' actual) +LoanDesk -> Catalogue.copy -LoanDesk -> Catalogue.copy

The refactoring is flagged, correctly in a literal sense and unhelpfully in a practical one: the two lookups are independent, and the order does not matter. That is the cost of a strict conformance test, so use it for sequences where order is the point (a protocol, a payment flow), not for every diagram. Be clear about what it cannot do: it checks the calls this one scenario makes, so it says nothing about the other paths, and a passing check does not make the design good.

When a Sequence Diagram Is Too Big

A faithful trace of one short desk session (three borrows and one return) is this big:

one desk session (3 borrows + 1 return): 41 calls, 73 arrows in all, fine 30p for comparison, the hand-drawn 'borrow' diagram has 11 arrows (7 of them calls) rendered size (width x height, px): 1568.5 x 3759

Seventy-three arrows, over 3,700 pixels tall: nobody will read it. The hand-drawn diagram has 11 arrows because its author decided what mattered. The same trace drawn at four levels of detail (the call depth kept, from the exercise 3 output):

full trace: 73 arrows (41 calls) detail kept arrows what a reader sees depth <= 1 8 the desk's four requests only depth <= 2 32 plus what each request asks of the catalogue and member depth <= 3 45 plus the member's own checks and loan lookups depth <= 9 73 everything

The ways to stay readable:

  • One scenario per diagram. Borrow, and return, and refusals, not a whole session.
  • Choose the level. Showing only the first two levels of calls (32 arrows here) tells the story; the third level is for the person implementing the member's checks.
  • Hide helpers, show collaborations. A call that carries a design decision stays; a loop's housekeeping goes.
  • Use ref (in PlantUML) to name a sub-scenario and draw it once.
  • Fewer participants. If five objects are not needed to make the point, leave them out and say so.
  • Prefer a different diagram when the point is the set of states (chapter 8) or the shape of a workflow (activity diagram) rather than who calls whom.

Communication Diagrams, in a Paragraph

A communication diagram (called a collaboration diagram in UML 1) carries the same information as a sequence diagram but draws the objects as boxes joined by lines, with numbered messages along the lines instead of a time axis. It shows which objects are linked more clearly and the order less clearly. It is a sensible choice for a small group of objects where the links matter. I did not draw or render one for this chapter, and this paragraph is from general knowledge; in practice most teams use sequence diagrams.

Usual Mistakes

MistakeWhy it hurtsFix
Drawing every callUnreadable, and out of date within a weekOne scenario; hide helpers; choose a level
Messages that are not operations"Check everything" is not a method the receiver can haveName each message as the operation the receiver will implement
Everything sent to one "manager" objectThe anemic service of chapter 6 again, drawn as a starIf one lifeline receives every message, ask who has the data (Expert)
Arrows pointing the wrong wayThe arrow goes from the caller to the object that has the operation, not from data to its userRead each arrow as "A asks B to..."
No failure pathThe diagram shows the sunny day the use case already describedAdd the extensions that matter as alt
A diagram nobody checks against codeIt describes the design as intended, not as builtTrace, or review with the code open; date the diagram
Mistakes while preparing this chapter
  • My own probe of Mermaid was wrong. I tested several features in one diagram, and the first run failed with "Trying to inactivate an inactive participant". I had written M-->>-D (which deactivates M) after activating D. The error was mine, not Mermaid's, and I nearly concluded a feature was missing. I fixed the diagram and re-ran: create, async, activation, loop, opt and par all work; only ref fails. Takeaway: test one new thing at a time, and make sure a failure is caused by the thing you are testing.
  • The tracer silently missed two things on its first run. It drew no creation of a Loan (a dataclass builds its __init__ with exec, so the function has no file name and my filter rejected it) and no refusal arrows (I tested whether an exception was in flight with sys.exc_info, which apparently did not report one inside a trace callback; removing that test fixed it). I only noticed because I read the generated diagram instead of trusting the count. Takeaway: a recorder that is wrong looks like a quiet program; read its output against a case where you know the answer.
  • A number I typed instead of computing. My first draft said the hand-drawn diagram has 8 arrows. It has 11. I had written the figure into a print statement from memory. The script now counts the lines.
  • A scripted edit that corrupted its own file, for the third time. I patched conformance.py with a Python script containing \n inside a string that was itself inside a heredoc; the newlines became real line breaks and the file would not parse (SyntaxError: unterminated string literal). I had made the same mistake twice before, while writing the chatbot course. How to avoid it: for edits that contain escape sequences, use the editor's replace tool, and run the file immediately after any scripted edit.

Hands-On Exercises

Exercise 1

Draw the sequence diagram for "Return a Book" (the main scenario, with a late return) in Mermaid, using chapter 6's design, from reading the code, without running it. Then check it against the real code with the tracer and say what differed, if anything.

๐Ÿ“„ View solution
Exercise 2

Draw "send overdue notices each night": a timer triggers the desk; for each overdue loan an email is sent to the member asynchronously and, only if the member has a mobile number, a text as well. Then check which operations the diagram needs that the class diagram lacks.

๐Ÿ“„ View solution
Exercise 3

Trace a desk session (three borrows, one return) and draw it at four levels of detail by limiting the call depth. Report how many arrows each has, and say which level you would put in a design document and which you would never publish.

๐Ÿ“„ View solution

Chapter 7 Quick Reference

  • Participants across the top, time down; ->> call, -->> return, -) asynchronous, activation bars, self messages, creation, a cross for a failure
  • Method: one scenario; name actor, controller and the objects that hold the responsibilities; each step becomes a message to the object that should do it; read it aloud as the use case
  • Fragments: alt, opt, loop, par in Mermaid; ref only in PlantUML (Mermaid 12.1.0 fails on it)
  • Every message is an operation on the receiver: compare the diagram with the class diagram to find missing operations and missing classes
  • Check a diagram against the code by tracing (sys.settrace); a diagram can be a test, but order-strict tests flag harmless refactorings
  • Too big? One scenario, a chosen level of detail (here 73 arrows down to 32 at two levels), hide helpers but not design-defining calls, use ref, fewer participants
  • Communication diagrams show the same facts around links rather than time (not drawn here)
Coming next
Object-Oriented Design with UML 8 (State Machines and Activity Diagrams): states, events, guards, actions, entry and exit, composite states and history; the lifecycle of a loan as a state machine, implemented and tested; the link to the State pattern in Design Patterns 8; activity diagrams with decisions, fork and join and swimlanes, and modelling reservation handling.