Detailed Table of Contents
Guidance for the item(s) below:
The next UML model we'll be learning is sequence diagrams. As before, focus on learning how to interpret these diagrams as you'll need to interpret some sequence diagrams in tP documentation pretty soon. Drawing sequence diagrams will be covered in a future week.
Sequence diagrams model the interactions between various entities in a system, in a specific scenario. Modeling such scenarios is useful, for example, to verify the design of the internal interactions is able to provide the expected outcomes.
Example Modeling how components of a system interact with each other to respond to a user action.
Example Modeling how objects inside a component interact with each other to respond to a method call it received from another component.
UML Sequence Diagrams → Introduction
UML Sequence Diagrams → Basic Notation
UML Sequence Diagrams → Loops
UML Sequence Diagrams → Object Creation
UML Sequence Diagrams → Minimal Notation
UML Sequence Diagrams → Object Deletion
UML Sequence Diagrams → Self-Invocation
UML Sequence Diagrams → Alternative Paths
UML Sequence Diagrams → Optional Paths
UML Sequence Diagrams → Calls to Static Methods
Guidance for the item(s) below:
The tP developer guide also has something called an architecture diagram. Let's learn how to interpret them too (drawing them will be covered in a future week).
When you write a small program by yourself, you can hold its structure in your head. You know where each piece of functionality lives, and you can usually predict what a change will affect.
That becomes much harder as the software and its team grow. Several people add code to the same program at once, and none of them has read all of it. Questions that once seemed trivial start to matter:
Software architecture is the set of significant decisions about a system's overall organization. It identifies the major parts, their responsibilities, how they interact, and how they are deployed.

Example Consider a desktop invoice-management application in which users create invoices, set due dates, and mark invoices as paid. A possible architecture has four parts:
Each arrow in the diagram is a dependency: the part at the tail needs the part at the head.
Ui: displays information and receives user inputLogic: interprets user actions and carries out operationsModel: represents invoices and other data while the program runsStorage: reads data from and writes it to persistent storageAn architecture description deliberately leaves out most of what is inside each part. It does not say how many classes are in Logic, which one validates a user command, or which collection holds the invoices. Those are detailed design decisions.
Architecture and detailed design differ in how far a decision reaches, not merely in how big it is. An architectural decision constrains what the other parts can do, shapes the qualities the system can achieve, and is expensive to reverse once the rest of the system is built on it.
Example Deciding that persistent data belongs in a separate Storage component is architecture. Deciding that Storage uses one class per file format is detailed design.
The boundary is not absolute. An internal choice becomes architecturally significant when it strongly affects the rest of the system.
Example A change to the data representation to make the application able to handle a million invoices can be architecturally significant, affecting more than the internals of the Storage component.
Every software system has an architecture, even when nobody designed one deliberately. An unplanned program still has parts and dependencies; they are simply accidental, undocumented, and usually tangled. The value of deciding an architecture on purpose is that the team shares one, understands it, and can reason about it.
An architecture is a shared and evolving technical understanding, not one person's private plan. A team sets an initial architecture early — the decisions are expensive to reverse later — then tests it against real requirements and revises it deliberately when those requirements change. Larger organizations may give one person an architect role to hold the technical vision, but the architecture still has to be understood by everyone building the system.
An architecture is also more than a diagram. Although an architecture is often represented by a diagram (like the one in the example above), a diagram shows one view. The architecture also includes the constraints everyone must work within and the reasons behind the choices.
Example The fact that the application stores data locally because it must work without a network connection is also part of the architecture, although it may not be captured by the architecture diagram.
The software architecture of a program or computing system is the structure or structures of the system, which comprise software elements, the externally visible properties of those elements, and the relationships among them. Architecture is concerned with the public side of interfaces; private details of elements—details having to do solely with internal implementation—are not architectural.
-- Software Architecture in Practice (2nd edition), Bass, Clements, and Kazman
The definition above is widely quoted in the literature. "Externally visible properties" means what promises to do, not how it does it.
A useful architecture limits how far many changes spread through the system.
Example Consider the following changes to an invoice manager application:
Storage, as long as its existing interface can still express what is needed.Ui; both interfaces can drive the same Logic.Logic does not depend on Ui, tests can call it directly.Architecture does not restrain the spread of every change. That is not necessarily an architectural failure: some changes genuinely cross several responsibilities.
Example Adding a new property to every invoice can touch the interface, the rules, the model, and the storage format.
Architecture also shapes the system's quality attributes — the properties that decide whether the software is good enough for its purpose. Common quality attributes include:
Quality attributes often conflict, and that is the central difficulty of architecture. Splitting a system across machines can allow independent scaling while making it slower and adding network failures. Adding component boundaries can limit how far a change spreads while adding interfaces that developers must learn and maintain.
There is no architecture that is best on every attribute — only one that fits the qualities and likely changes that matter for the system being built.
Three words do most of the work in architecture descriptions: components, interfaces, dependencies.
A component is a major part of the system with one coherent responsibility.
Example Storage's work concerns persistent data. A single Invoice class is too small to be an architectural component.
An interface is the agreement stating how the rest of the system may use a component. It is more than a list of operations. A complete interface also covers:
Example Storage might offer saveInvoices(invoices) and readInvoices(), and specify that a corrupt file causes a particular error rather than a crash.
A dependency exists when one component relies on another to do its job. Dependencies have a direction, and that direction matters more than almost anything else in an architecture.
Example Logic depends on Storage, because it cannot save without it. Storage does not depend on Logic: it can be compiled, tested, and understood without knowing that commands exist.
A component is not a special programming construct. There is no component keyword; a component is whatever unit of code the team agrees to treat as one part with one responsibility. It may be a separate library, a language-level module, or an entirely separate program reached over a network. Its agreed responsibility and interface make it a component, not its folder layout.
Example In a Java project a component is often a package (or group of packages) plus a type declaring what it offers:
A dependency arrow describes reliance, not necessarily a method call. Whether that reliance is a method call, a message, or a network request is exactly what the legend must tell you.
Example If a diagram shows Logic depends on Storage and its legend says an arrow means depends on, then some code in Logic relies on what Storage offers, and no code in Storage relies on Logic.
Depending on the interface rather than a specific implementation is what makes a component replaceable. Such a replacement works only if it honors the same behavior, including its failure behavior — matching method names is not enough.
Example If Logic relies on the Storage agreement rather than on JsonStorage directly, a DatabaseStorage can take its place with few changes elsewhere.
The same system can be drawn in more than one way, depending on what you want to show. Here are two commonly used views:
Example The same invoice manager, but two views of the architecture:
Each arrow in the logical view is a dependency. The deployment view has no arrows because all four parts run inside one program on one computer.
That deployment view in the example looks trivial, which is exactly the point. Right now all the interesting structure is logical. When a server is added to let several users share invoices, both views change — the deployment view dramatically, the logical view in a smaller but real way.
Keeping the views separate prevents a common confusion.
Example "This system has four components" and "this system runs on four computers" are entirely different statements, and one diagram that blurs them will mislead everyone who reads it.
Formal frameworks define four, five, or more standard views. For example, the 4+1 view model.
Architecture diagrams have no universally adopted notation. Unlike UML class diagrams, there is no rulebook: different teams use different shapes, colors, and arrows. A reader must first work out what the notation means.
Read an architecture diagram by asking four questions in order:
A single diagram need not explain the whole architecture, but it must communicate its chosen view without ambiguity. Judge it against its stated purpose and the documentation it ships with: if the four questions cannot be answered from the diagram, its caption, and the text around it, something is missing — a useful thing to notice when reviewing a teammate's work.
Example Here are two real architecture diagrams, from actual projects, drawn by different teams in different notations. They describe systems unrelated to the invoice-manager example, which is exactly the situation you face when you join an unfamiliar project.


Try the four questions on both. They organize their boxes differently, use different shapes, and label their arrows differently, yet both are legitimate. For each one, work out which of the four questions the diagram answers by itself, and which stay ambiguous until you read the documentation around it — real diagrams often leave some of the four to the accompanying text.
Guidance for the item(s) below:
As the tP is bigger than the iP, it's not possible to work with its entire design at the same time. The next topic explains a technique that can help when dealing with the design of a bigger system.
In a smaller system, the design of the entire system can be shown in one place.
Example This class diagram of se-edu/addressbook-level2 depicts the design of the entire software.

The design of bigger systems needs to be created and shown at multiple levels.
Example This architecture diagram of se-edu/addressbook-level3 depicts the high-level design of the software.
Lower-level designs of some components of the same software:



The debugging topics of this textbook draw substantially on The Debugging Book by Andreas Zeller et al., in particular its terminology of defects, infections, and failures, and its treatment of debugging as an application of the scientific method. Further material was adapted from the debugging readings of MIT 6.102, UC Berkeley CS61B, Stanford CS107, UW CSE 332, and CMU 15-213.
Debugging is the process of finding the cause of a known problem in a program, and fixing it. It starts after you know something is wrong — whether a test, a user, or monitoring exposed it; finding that the problem exists is a separate activity. The hard part is usually the diagnosis rather than the correction: once you understand why a program misbehaves, the edit itself is often a single character — though choosing which edit is a decision in its own right.
To debug well, distinguish four things that beginners tend to lump together as 'the bug':
1.These four form a chain, and each link can be far from the next:
mistake → defect → infection → infection → ... → failure
(code) (state) (state) (behavior)
Debugging is therefore a search, not a lookup: you observe the failure but must fix the defect. The infection spreads as the wrong value is passed on, stored in a field, or used to compute another wrong value, so where the program crashed is usually not where the mistake was made. The chain also explains why bugs hide: a defect infects the state only when that line executes, and an infection becomes a failure only if it propagates out to something observable. A defect can sit in daily-executed code for months unnoticed.
The chain assumes the fault lies in code — the common case, but not the only one. A failure can equally originate in configuration, in data left by an earlier version, in a dependency, in the deployment, or in a requirement that was wrong to begin with. Sometimes the program is right while the test is wrong — the test may hold its own defect, or the expectation it encodes may never have been correct. Only an error in the code is a defect; where the fault lies elsewhere, call it the cause. Either way the search is the same: locate whatever has to change.
Example A running example, reused throughout the related debugging topics of this textbook. A shopping cart prints the correct total, but then appears empty.
class Cart {
private final List<Item> items = new ArrayList<>();
void add(Item item) {
items.add(item);
}
List<Item> getItems() {
return items;
}
int computeTotal() {
int total = 0;
List<Item> pending = getItems();
while (!pending.isEmpty()) {
total += pending.remove(0).price();
}
return total;
}
}
Take the cart's intended contracts to be these: computing a total must not change the cart, and getItems() lets callers read the items without owning the list.
getItems() hands back a copy.getItems() hands out the live internal list, which computeTotal() then empties — breaking both contracts at once.computeTotal() returns, the cart's own items list is empty.A debugger stopped at the failure would be pointing at the display code, which is correct.
Debugging consumes a large share of real development effort, and a single stubborn defect routinely costs more than writing the code it hides in. Beginners tend to read time spent debugging as evidence that they are bad at programming. It is not; debugging is a distinct and learnable engineering skill.
Three things make it hard:
Debugging time is therefore not proportional to the size of the fix. A one-character correction can cost an afternoon. The cost lives in the search, and every debugging technique aims at making that search cheaper.
Most unproductive debugging comes from having no method, rather than from using the wrong tool.
catch block.Adding temporary print statements is not necessarily bad; doing so without a hypothesis is — that is shotgun debugging in another form. A few prints chosen to answer a specific question are legitimate, and in production, embedded, or concurrent settings they are sometimes the only tool available.
What these have in common is that they produce activity without producing information. A productive debugging step is one that rules something out.
Systematic debugging follows roughly this sequence:
The initials spell TRAFFIC, a widely used mnemonic for the debugging process due to Andreas Zeller.
Treat this as a map rather than a mandatory order. Steps 4 to 6 form a loop that turns several times before it lands on the cause, and you can reorder the earlier steps freely: simplifying often finds the origin for free, and a failed isolation sends you back for a better reproduction.
The most common mistake is jumping straight to step 7. Starting at 'correct' and working backwards is how shotgun debugging happens. This unit covers steps 1 to 6; step 7 is covered separately.
Steps 1 to 6 of TRAFFIC are the scientific method applied to a program: you have an unexplained phenomenon, you propose an explanation, and you test it. Applying it deliberately is what separates systematic debugging from guesswork; every technique that follows exists to make one turn of this loop cheaper.
items is empty by the time computeTotal() returns" is.Keep a debugging log. One line per hypothesis, prediction, observation, and conclusion sounds bureaucratic, but it stops you re-testing rejected explanations, survives interruptions, and is what you hand over when the bug becomes someone else's. Start one as soon as the investigation will outlast a few hypotheses, or as soon as you catch yourself repeating a probe.
Example A debugging log for the cart example, in which computeTotal() empties the very list that getItems() handed it:
| # | Hypothesis | Prediction | Observation | Conclusion |
|---|---|---|---|---|
| 1 | add() never stored the items | items.size() == 0 right after adding | size() == 3 | Rejected |
| 2 | Something empties the list during computeTotal() | size() drops from 3 to 0 across the call | 3 before, 0 after | Supported — narrowed to that method |
| 3 | pending and items are the same object | the two identities match when stepping into the loop | same object | Supported — and it accounts for the whole failure: correct total, then an empty cart |
Rejecting hypothesis 1 is what suggested hypothesis 2.
Know when to stop for the day: Debugging is unusually sensitive to fatigue, because the whole activity consists of holding a model of the program in your head.
Debugging starts with being able to state what the correct behavior is, and why. Without that you have nothing to compare the program against, and you risk searching code that was right all along. State the expectation in the form the test will eventually take: for this input, that exact result.
Record the problem somewhere durable, so it survives an interruption — an issue tracker entry for anything beyond a few minutes of work, or a note beside you for the rest. A useful record holds the expected behavior, the actual behavior, the conditions under which you saw it, the steps to reproduce it, and the smallest failing case you have. This is not the same thing as a debugging log: the record holds the problem, while the log holds the investigation, one line per hypothesis and what it settled.
Sometimes the fault is in the test, not in the code under investigation: the test may hold its own defect, or its expectation may be wrong. Worth considering early, because it is easy to lose hours to a test that was wrong all along.
A reliable reproduction is the most valuable thing you can have, because it makes every experiment cheap and is the surest way to confirm afterwards that the fix worked.
Reproducing means recreating everything the failure depends on, usually more than the input alone: the input data, the program version, the environment and configuration operating system, locale, file paths, settings, the sequence of actions, and the starting state, such as leftovers from a previous run.
Build the reproduction deliberately rather than waiting for the failure to recur. Pin every source of nondeterminism you control — the random seed, the clock and time zone, iteration order, and the number of threads. Script the sequence of actions instead of performing it by hand, so it is identical every time. Reset to a known starting state before each attempt, so a leftover from the previous run cannot decide the outcome. And record the environment values from the run that failed, so you can restore them rather than guess at them.
When you cannot reproduce a failure you can still investigate it, but the work changes character. Instead of running experiments you mine the evidence left behind: stack traces, logs, crash dumps a snapshot of the process's state at the moment it died, thread dumps what every thread was doing or waiting for, and the differences between runs that failed and runs that did not. The immediate goal becomes making the failure more observable or more frequent — logging around the suspected area, tightening assertions, or finding the extra ingredient that decides between the two outcomes. Reproducibility is not all-or-nothing: moving a bug from 'once a week' to 'one run in five' is real progress. Without a reproduction you also confirm the fix differently: test the mechanism you believe was wrong, then watch for recurrence over a period long enough to mean something.
Automate the reproduction as a test case as early as you can. Turning "launch the app and perform these six steps" into a one-second command is what makes the hypothesis loop affordable, and it becomes the regression test once you have a fix. You will run it dozens of times before you are done.
The smaller the failing case, the smaller the search space — every element you can remove while the failure persists eliminates a whole category of possible causes.
Try halving the input first: cut it in half, test each half, keep whichever still fails, repeat. When it works it is very cheap, and needs no insight into the code.
But halving frequently does not work. Sometimes neither half fails, because the failure needs two elements the cut separated. Sometimes both fail for an unrelated reason, because half an input is not valid input — half a Java file does not compile, half a config file lacks its required header. Then remove smaller pieces one at a time, preserving whatever structure the format demands.
Simplify the code path too, not just the data: strip away unrelated features, configuration, and calls until only the failing core remains. A failure that survives is far easier to reason about; one that does not has told you something about what it depends on.
Example A 500-line configuration file makes the app crash at startup. Halving gets nowhere: neither half crashes, because the failure needs one setting from each. Removing settings one at a time from the full file isolates the pair — a theme entry and a locale entry, each harmless alone. Every trial file must keep the required header, or the app rejects it for an unrelated reason and the experiment tells you nothing.
A good bug report is a reproduction that someone else can run. The work of reproducing and simplifying is the content of the report — which is why producing a minimal example so often solves the problem before it is filed. When you cannot reproduce a failure, report the evidence you do have logs, stack traces, the conditions under which it appeared rather than nothing.
An origin is a place where the state could first have gone wrong: before it the state is correct, after it the state is infected, and the cause sits at that boundary. This step aims at a list of candidate origins rather than a single answer — a search that begins with one suspect usually ends by wrongly confirming that suspect.
Candidate origins are not equally likely, and the order you check them in decides how long the search takes.
Isolating means running one check, discarding the part of the search space it rules out, and repeating until the boundary narrows to a single statement.
git bisect automates this, and works best with small, self-contained commits; one you cannot build or test must be skipped, leaving several candidates rather than one. Martin Fowler calles this Diff Debugging.Example Binary search along the execution, on a run too long to watch: a 10,000-row import produces the right running total at the start and the wrong one at the end, and nothing in between is visible. Pause at row 5,000 and ask one question — is the total already wrong? If it is, the cause lies in the first half, so pause next at row 2,500; if it is not, pause at row 7,500. Fourteen such checks reduce 10,000 rows to one, and none of them requires understanding the code — only the ability to say whether the state is already wrong.
The cheapest bug to debug is the one that announces itself, and most of what makes code debuggable is decided long before the bug exists.
@Override, generics, final. A defect caught here costs no debugging at all.Each of these shortens the distance between defect and failure, which is the root of the difficulty.
Do not settle on a fix until you can explain the whole failure. Temporary changes made as experiments are fine, but a change you intend to keep needs a causal account that explains every observed behavior. The strongest check is to predict, before making the change, exactly what will be different afterwards, then verify that prediction. A fix that works for reasons you cannot state will come back.
Fix the cause, not the infection and not the failure. Special-casing the failing input or clamping a bad value removes the symptom and leaves the cause. Also consider whether you have a coding error or a design error: a coding error is a defect — the code does not do what you intended — whereas a design error means the intention itself was wrong. The second cannot be repaired at the site of the failure — patching there breeds special cases, and the real remedy is a design change. These are the two common cases, not the only ones: the cause can equally sit in configuration, data, a dependency, or the requirement, and then the correction belongs there.
Which change counts as 'the fix' is sometimes a genuine choice. Making that choice consciously, rather than patching whichever line you happened to be looking at, is part of fixing properly.
Example In the cart example you could make getItems() return a copy, or make computeTotal() iterate without mutating. Both remove the failure; they differ in which contract you treat as authoritative.
Once you have a candidate fix, finish the job:
Example The cart example, end to end:
computeTotal() empties the cart every time.computeTotal(), then asserts on getItems().size(). One item is enough to fail, so the test uses one.items is the loop in computeTotal(), and size() dropping from 3 to 0 across the call — with pending and items confirmed to be the same object — settles it.getItems() returning a copy and computeTotal() iterating without mutating would remove the failure, so the real question is which contract to treat as authoritative. getItems() is an accessor, and an accessor that hands back live internal state makes every caller a potential mutator — so that is the one to change, and it returns List.copyOf(items). Check the relatives while you are there: any other getter on the class that returns an internal collection has the same problem. The test now passes, the rest of the suite still passes, that test stays behind as the regression test, and the breakpoints used while isolating come out.Every way of looking inside a running program is a probe — a means of answering one specific question about its state. The useful question is never "print statements or debugger?" but "what is the cheapest probe that answers this question?" Some probes come out once the bug is found (e.g., a breakpoint or a temporary print statement); others are meant to stay (e.g., a permanent log statement added at a component boundary).
assert statement, enable assertions in your run configuration (-ea) or it will do nothing; test-framework assertions are separate and always run.As a rough guide:
A debugger lets you pause a running program, then inspect and control it from the inside, without modifying its code. That last part is what makes it different in kind from printing: asking one more question costs seconds rather than another edit-build-run cycle.
Breakpoints determine where the program pauses.
i == 4137.null?"Disable breakpoints rather than deleting them, so that a debugging session can be paused and resumed.
Stepping commands determine how execution advances. step over runs the next line, including any call it makes, as one step. step into enters the method being called. step out finishes the current method and pauses at its caller. run to cursor continues to a chosen line.
The inspection views tell you what state the program is in.
Example One session on the cart example, from the first breakpoint to the diagnosis:
total += pending.remove(0).price();, then run the code that adds three items and calls computeTotal().pending holding all three items, and nothing yet looks wrong.pending == items answers true. That single evaluation is the diagnosis — the list being emptied is the cart's own — and it cost no edit, no rebuild, and no re-run.computeTotal() called from the display code, so the frame the failure will surface in is not the frame the defect is in.items.size() fall in the variables view as the loop runs.A field watchpoint on items would not have helped here: items is assigned once, where it is declared, so the watchpoint fires at construction and never for remove(0). A watchpoint catches a field being reassigned, not the object it already points at being modified.
Two habits are worth forming:
AI assistants are useful for some parts of debugging and unreliable for others. They are good at explaining unfamiliar error messages, proposing candidate hypotheses, and serving as an always-available rubber duck. They are unreliable at diagnosing a defect in code they cannot run, and will produce confident, fluent, incorrect explanations. A systematic method is what makes them safe: treat any suggestion as a hypothesis, insist it be falsifiable, and verify it against the running program yourself.
A stack trace is a precise report of where a program failed and the call path that led there — yet beginners routinely scroll past it. Note its limit: the call path is exact, but how the program came to be in that state is not in the trace.
Read it in this order:
The top of the trace is where the failure surfaced, but the cause is often further down, in whichever frame passed the bad value along. In wrapped exceptions, read the Caused by: chain from the bottom up.
| Exception | Usually means |
|---|---|
NullPointerException | Something never initialized, or a method returning null unnoticed |
IndexOutOfBoundsException | An off-by-one, or an index computed from stale size information |
ClassCastException | An object that is not the type assumed, often after an unchecked cast |
ConcurrentModificationException | A collection modified while being iterated over, usually in a single thread Example removing from a list inside a for-each loop over that list |
StackOverflowError | Recursion with a missing or unreachable base case |
NumberFormatException | Unvalidated input being parsed as a number |
Guidance for the item(s) below:
AI's impact on » IDEs
AI changes what you use an IDE for, not whether you need one.
Refer to these se-edu guides:
Refer to these se-edu guides:
Guidance for the item(s) below:
The tP uses logging as one of its error handing strategies. As you'll be reading tP code soon, let's make sure you can recognize logging code when you see them.
Logging is the deliberate recording of certain information during a program execution for future reference. Logs are typically written to a log file, but it is also possible to log information in other ways e.g., into a database or a remote server.
Logging can be useful for troubleshooting problems. A good logging system records some system information regularly. When problems occur in a system e.g., an unanticipated failure, the associated log files may indicate what went wrong, and actions can then be taken to prevent it from happening again.
A log file is like the of an airplane; it does not prevent problems, but it can be helpful in understanding what went wrong after the fact.
source: https://commons.wikimedia.org
Most programming environments come with logging systems that allow sophisticated forms of logging. They have features such as the ability to enable and disable logging easily or to change the logging .
Example This sample Java code uses Java’s default logging mechanism.
First, import the relevant Java package:
import java.util.logging.Level;
import java.util.logging.Logger;
Next, create a Logger:
private static Logger logger = Logger.getLogger("Foo");
Now, you can use the Logger object to log information. Note the use of a for each message. When running the code, the logging level can be set to WARNING so that log messages specified as having INFO level (which is a lower level than WARNING) will not be written to the log file at all.
// log a message at INFO level
logger.log(Level.INFO, "going to start processing");
// ...
processInput();
if (error) {
// log a message at WARNING level
logger.log(Level.WARNING, "processing error", ex);
}
// ...
logger.log(Level.INFO, "end of processing");
Tutorials:
Best Practices: