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. 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 concepts appear frequently 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.
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.
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 usually indicated in the .
Architecture diagrams can differ in what they show and how much detail they show. First consider two common views of a system:
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.
In this example, all the interesting structure is in the logical view. If a server is added to let several users share invoices, both views change. The deployment view changes dramatically; the logical view changes 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.
The 4+1 view model extends the idea above by describing a system using five standard views. Four cover the system's functionality, runtime processes, code organization, and deployment. The "+1" uses scenarios to show how the system works in specific situations.
Switching to the aspect of how much details a diagram show, the design of a small system can be shown at a single level of detail. One diagram can cover the entire system.
Example This class diagram of se-edu/addressbook-level2 depicts the design of the entire software.

The design of a bigger system needs to be created and shown at multiple levels: higher-level diagram shows the major parts, and lower-level diagrams zoom into individual parts to show their internal design.
Example This architecture diagram of se-edu/addressbook-level3 depicts the high-level design of the software.

dashed arrows: dependencies
solid arrows: associations
Lower-level designs of some components of the same software:



The C4 model is a specific form of multi-level design views. It organizes diagrams of a system's static structure into four core levels, which zoom in from the system's surroundings to its code:
Example The addressbook-level3 diagrams above roughly match the two lowest C4 levels: the architecture diagram is a component diagram, and the class diagrams are code diagrams.
Neither 4+1 nor C4 is a new diagram notation. 4+1 organizes different kinds of architectural information, while C4 provides zoom levels for static structure. Their diagrams can use an established notation such as UML, or an ad hoc notation made up of boxes, arrows, and text labels.
Both models are meant for large systems, larger than our invoice manager example. They add value when a system has many parts running in several places, and its architecture must be explained to readers who each need a different slice of it.
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:
Example Here are two real architecture diagrams, from actual projects, drawn by different teams in different notations.


dashed arrows: dependencies
solid arrows: associations
Follow up notes for the item(s) above:
AI's impact on » Architecture Diagrams
AI makes architecture diagrams easier to produce, but more important to check. An agent can inspect code and quickly draft or update diagrams, so drawing every box and arrow by hand matters less. However, it may choose the wrong view, include too much detail, or mistake the current code structure for the intended architecture.
As agents make larger changes across unfamiliar code, developers still need to select the right view and level of detail, explain the notation, and verify that each diagram accurately communicates the system’s architecture.
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:
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. The hard part is usually the diagnosis rather than the correction.
To debug well, we distinguish four things that we loosely refer to as 'the bug':
1.These four form a chain, and each link can be far from the next:
A big part of debugging is the backward search for the defect by starting from the observed failure.
This 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. So, a defect can sit in daily-executed code for months unnoticed.
Example The shopping cart code given below prints the correct total, but then the cart 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;
}
}
Cart's intended contracts: 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.Some factors that make debugging hard are:
catch block.What these have in common is that they produce activity without producing useful information. A productive debugging step is one that rules something out.
Adding temporary print statements is not necessarily bad. 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. However, this technique alone is not enough to tackle most non-trivial bugs.
Systematic debugging involves following tasks, whose first letters make up the mnemonic TRAFFIC (credit: Andreas Zeller).
TRAFFIC is a map rather than a mandatory sequence. Some tasks can be done in a loop before it lands on the cause, and you can reorder the other tasks as you see fit.
The most common mistake is jumping straight to C. Starting at 'correct' and working backwards is how shotgun debugging happens. This text below covers the six tasks TRAFFIC except C, which is covered separately.
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.
The first step of debugging should be stating 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 a testable form: for this input, that exact result.
Sometimes the fault is in the test, not in the code under investigation. It is worth considering early, because it is easy to lose hours to a test that was wrong all along.
A reliable reproduction makes every experiment cheap and is a way to confirm afterwards that the fix worked. Reproducing means recreating everything the failure depends on, e.g., the input data, the program version, the environment and , the sequence of actions, and the starting state, such as leftovers from a previous run.
When you cannot reproduce a failure you can still investigate it. Instead of running experiments you mine the evidence left behind: stack traces, logs, , , and the differences between runs that failed and runs that did not. The immediate goal becomes making the failure more observable or more frequent. Moving a bug from 'once a week' to 'one run in five' is real progress.
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 cheap, and it can become the regression test once you have a fix.
Why simplify? 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. Some directions you can try:
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.
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 often produces a list of candidate origins rather than a single answer. Some techniques you can follow:
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 you find what needs to be fixed.
git bisect automates this, and works best with small, self-contained commits. Martin Fowler calls 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.
Covered in a separate section, titled Fixing.
Every way of looking inside a running program is a probe — a means of answering one specific question about its state. Here are some widely used probes:
assert statement, enable assertions in your run configuration or it will do nothing; test-framework (e.g., JUnit) assertions are separate and always run during testing.The useful question is never "print statements or debugger or ___?" but "what is the cheapest probe that answers this question?" Some probes come out once the bug is found (e.g., a temporary print statement); others are meant to stay (e.g., a permanent log statement).
As a rough guide:
A debugger is a tool that lets you pause a running program, then inspect and control it from the inside, without modifying its code. You choose the lines where the program should stop. When execution reaches one of them, the program freezes mid-run and waits for you. You can then look at the current values of its variables, see the chain of method calls that led there, and let it continue one line at a time. Most IDEs, such as IntelliJ IDEA, come with a debugger built in. There are also standalone debuggers that you control by typing commands, such as GDB for C and C++, and jdb for Java.
If you have not used a debugger tool before, watching a 'debugger in action' video such as the one below is recommended before reading the rest of this topic.
Debugging in IntelliJ
A debugger works from outside your program, which is what makes it different in kind from printing. The debugger runs as a separate process. It either launches your program under its control or attaches to one that is already running, then asks the operating system, or the JVM for a Java program, to pause the program at the chosen lines and report its state back. Your code stays untouched throughout. With print statements, every new question about the program means adding another print, rebuilding, and re-running. With a debugger, the paused program keeps all its state, so asking one more question costs seconds rather than another edit-build-run cycle.
Breakpoints determine where the program pauses. A breakpoint is a marker that tells the debugger to stop the program at a particular spot. In an IDE, you usually set one by clicking in the margin next to a line number, which puts a red dot there. In GDB, you type a command such as break main.c:42 instead. Either way, the debugger keeps track of the breakpoint, so your source file does not change. Breakpoints take effect only when you start the program through the debugger, e.g., with IntelliJ's Debug button rather than Run. Started the normal way, the program runs straight past them. Breakpoints come in several kinds:
i == 4137.null?"Stepping commands determine how execution advances. Once you have examined the paused program, a stepping command lets it run a little further and then pauses it again, so you can see what changed. In an IDE, each command has a toolbar button and a keyboard shortcut. The common ones are:
total += item.price();, step over pauses at the line after it, while step into pauses inside price().Inspection views let you examine details of the state the program is currently paused at. Some common inspection views:
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.
Set your first breakpoint before the suspected region rather than at the failure, so you can watch the state go wrong.
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. 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.
Some common Java exceptions and what they usually mean:
| 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 |
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 can fail again.
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 code that does not do what you intended -- whereas a design error means the intention itself was wrong. The real remedy for the latter is a design change, not a code fix.
Which change counts as 'the fix' is sometimes a genuine choice. Making that choice consciously helps ensure that the fix does not degrade other aspects of the system (e.g., its design integrity).
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. Choose one and implement it. Follow up notes for the item(s) above:
AI's impact on » Debugging
AI can help with the grunt work of debugging, but being skilled in systematic debugging still matters. Agents can explain error messages, inspect unfamiliar code, suggest possible causes, and generate tests or probes, so routine searching and debugger setup matter less. However, an agent can confidently give a wrong diagnosis or change code until the symptom disappears without finding the cause.
As developers debug more code they did not write, they need good debugging skills to be able to reproduce failures, test hypotheses against the running program, and verify that each fix removes the real cause.
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: