UML Sequence Diagrams


Unlocking System Behavior with UML Sequence Diagrams

Introduction: The “Who, What, and When” of Systems

Imagine walking into a coffee shop. You place an order with the barista, the barista sends the ticket to the kitchen, the kitchen makes the coffee, and finally, the barista hands it to you. This entire process is a sequence of interactions happening over time.

In software engineering, we need a way to visualize these step-by-step interactions between different parts of a system. This is exactly what Unified Modeling Language (UML) Sequence Diagrams do. They show us who is talking to whom, what they are saying, and in what order.

Learning Objectives

By the end of this chapter, you will be able to:

  1. Identify the core components of a sequence diagram: Lifelines and Messages.
  2. Differentiate between synchronous, asynchronous, and return messages.
  3. Model conditional logic using ALT and OPT fragments.
  4. Model repetitive behavior using LOOP fragments.

Part 1: The Basics – Lifelines and Messages

To manage your cognitive load, we will start with just the two most fundamental building blocks: the entities communicating, and the communications themselves.

1. Lifelines (The “Who”)

A lifeline represents an individual participant in the interaction. It is drawn as a box at the top (with the participant’s name) and a dashed vertical line extending downwards. Time flows from top to bottom along this dashed line.

2. Messages (The “What”)

Messages are the communications between lifelines. They are drawn as horizontal arrows. UML 2 distinguishes three main arrow styles (sources: Fowler, UML Distilled, ch. 4; Rumbaugh, Jacobson & Booch, The Unified Modeling Language Reference Manual):

  • Synchronous Message — solid line with filled (triangular) arrowhead. The sender blocks until the receiver responds, like calling a method and waiting for it to return.
  • Asynchronous Message — solid line with open (stick) arrowhead. The sender fires the message and continues immediately, like putting an event on a queue or sending an HTTP POST without awaiting the response.
  • Return Message dashed line with open arrowhead. Represents control (and often a value) returning to the original caller. Return arrows are optional in UML 2: include them when the returned value is important, omit them when a synchronous call obviously returns.

⚠ Common mistake: Students often confuse the filled vs. open arrowhead, treating both as synchronous. The rule: filled = blocks, open = fires-and-forgets. Remember it as “filled is full commitment; open lets go.”

Visualizing the Basics: A Simple ATM Login

Let’s look at the sequence of a user inserting a card into an ATM.

(1) insertCard() (2) verifyCard() (3) cardValid() (4) promptPIN() customer: Customer atm: ATM bank: Bank Server

Notice the flow of time: Message 1 happens first, then 2, 3, and 4. The vertical dimension is strictly used to represent the passage of time.

Stop and Think (Retrieval Practice): If the ATM sent an alert to your phone about a login attempt but didn’t wait for you to reply before proceeding, what type of message arrow would represent that alert? (Think about your answer before reading on).

Reveal Answer An asynchronous message, represented by an open/stick arrowhead, because the ATM does not wait for a response.

Part 1.5: Activation Bars and Object Naming

Now that you understand the basic elements, let’s add two important details that appear in real-world sequence diagrams.

Activation Bars (Execution Specifications)

An activation bar (also called an execution specification) is a thin rectangle drawn on a lifeline. It represents the period during which a participant is actively performing an action or behavior—for example, executing a method. Activation bars can be nested across software lifelines and within a single lifeline (e.g., when an object calls one of its own methods). Human actors are usually shown as initiators or recipients, not as executing software behavior, so they normally do not need activation bars.

requestStop() addStop() stopScheduled confirmation openDoors() requestClose() closeDoors() doorsClosed confirmation passenger: Passenger station: Station train: Train

The blue bars show when each object is actively processing. Notice how the Station is active from when it receives requestStop() until it sends the confirmation, and how the Train has separate execution bars for addStop(), openDoors(), and closeDoors().

Object Naming Convention

Lifelines in sequence diagrams represent specific object instances, not classes. The standard naming convention is:

objectName : ClassName

  • If the specific object name matters:
  • If only the class matters: (anonymous instance)
  • Multiple instances of the same class get distinct names:

This is different from class diagrams, which show classes in general. Sequence diagrams show one particular scenario of interactions between concrete instances.

Consistency with Class Diagrams

When you draw both a class diagram and a sequence diagram for the same system, they must be consistent:

  • Every message arrow in the sequence diagram must correspond to a method defined in the receiving object’s class (or a superclass).
  • The method names, parameter types, and return types must match between the two diagrams.

Part 2: Adding Logic – Combined Fragments

Real-world systems rarely follow a single, straight path. Things go wrong, conditions change, and actions repeat. UML uses Combined Fragments to enclose portions of the sequence diagram and apply logic to them.

Fragments are drawn as large boxes surrounding the relevant messages, with a tag in the top-left corner declaring the type of logic, such as , , , or .

Common fragment syntax in sequence diagrams:

  • Optional behavior:
  • Alternatives with guarded branches:
  • Repetition:
  • Parallel branches:
  • Early exit:
  • Critical region:
  • Interaction reference:

1. The OPT Fragment (Optional Behavior)

The opt fragment is equivalent to an if statement without an else. The messages inside the box only occur if a specific condition (called a guard) is true.

Scenario: A customer is buying an item. If they have a loyalty account, they receive a discount.

OPT calculateTotal() subtotal applyDiscount() discountApplied() checkout: Checkout System pricing: Pricing Engine [hasLoyaltyAccount == true]

Notice the [hasLoyaltyAccount == true] text. This is the guard condition. If it evaluates to false, the sequence skips the entire box.

2. The ALT Fragment (Alternative Behaviors)

The alt fragment is equivalent to an if-else or switch statement. The box is divided by a dashed horizontal line. The sequence will execute only one of the divided sections based on which guard condition is true.

Scenario: Verifying a user’s password.

ALT checkPassword() loginSuccess() checkPassword() loginFailed() system: System db: Database [password is correct] [password is incorrect]

3. The LOOP Fragment (Repetitive Behavior)

The loop fragment represents a for or while loop. The messages inside the box are repeated as long as the guard condition remains true, or for a specified number of times.

Scenario: Pinging a server until it wakes up (maximum 3 times).

LOOP ping() ack() app: App server: Server [up to 3 times]

Part 3: Putting It All Together (Interleaved Practice)

To truly understand how these elements work, we must view them interacting in a complex system. Combining different concepts requires you to interleave your knowledge, which strengthens your mental model.

The Scenario: A Smart Home Alarm System

  1. The user arms the system.
  2. The system checks all windows.
  3. It loops through every window.
  4. If a window is open (ALT), it warns the user. Else, it locks it.
  5. Optionally (OPT), if the user has SMS alerts on, it texts them.
OPT LOOP ALT armSystem() getStatus() statusData() warn() lock() sendText("Armed") user: User hub: Alarm Hub sensors: Window Sensors sms: SMS API [smsEnabled == true] [for each window] [status == "Open"] [status == "Closed"]

Part 4: Combined Fragment Reference

The three fragments above (opt, alt, loop) are the most common, but UML defines additional fragment operators:

Fragment Meaning Code Equivalent
ALT Alternative branches (mutual exclusion) if-else / switch
OPT Optional execution if guard is true if (no else)
LOOP Repeat while guard is true while / for loop
PAR Parallel execution of fragments Concurrent threads
CRITICAL Critical region (only one thread at a time) synchronized block
BREAK Early exit from the enclosing interaction break / early return
REF Reference to another sequence diagram by name Function / subroutine call

When to use ref: When a shared interaction (e.g., login, authentication, checkout) appears in many sequence diagrams, draw it once as its own diagram and reference it from others with a ref frame. This is the sequence-diagram equivalent of factoring out a function.


Part 5: From Code to Diagram

Translating between code and sequence diagrams is a critical skill. Let’s work through a progression of examples.

Example 1: Simple Method Calls

class Register {
    public void method(Sale sale, int cashTendered) {
        sale.makePayment(cashTendered);
    }
}

class Sale {
    public void makePayment(int amount) {
        Payment payment = new Payment(amount);
        payment.authorize();
    }
}

class Payment {
    Payment(int amount) { }

    void authorize() { }
}
class Payment {
public:
    explicit Payment(int amount) { }

    void authorize() { }
};

class Sale {
public:
    void makePayment(int amount) {
        Payment payment(amount);
        payment.authorize();
    }
};

class Register {
public:
    void method(Sale& sale, int cashTendered) {
        sale.makePayment(cashTendered);
    }
};
class Payment:
    def __init__(self, amount: int) -> None:
        pass

    def authorize(self) -> None:
        pass


class Sale:
    def make_payment(self, amount: int) -> None:
        payment = Payment(amount)
        payment.authorize()


class Register:
    def method(self, sale: Sale, cash_tendered: int) -> None:
        sale.make_payment(cash_tendered)
class Payment {
  constructor(amount: number) { }

  authorize(): void { }
}

class Sale {
  makePayment(amount: number): void {
    const payment = new Payment(amount);
    payment.authorize();
  }
}

class Register {
  method(sale: Sale, cashTendered: number): void {
    sale.makePayment(cashTendered);
  }
}
makePayment(cashTendered) payment: Payment <<create>> authorize() register: Register sale: Sale

Notice how the Payment constructor call becomes a create message in the sequence diagram. The Payment object appears at the point in the timeline when it is created.

Example 2: Loops in Code and Diagrams

import java.util.List;

class Item {
    int getID() { return 0; }
}

class SaleLine {
    final String description;
    final int total;

    SaleLine(String description, int total) {
        this.description = description;
        this.total = total;
    }
}

class B {
    void makeNewSale() { }

    SaleLine enterItem(int itemId, int quantity) {
        return new SaleLine("", 0);
    }

    void endSale() { }
}

class A {
    private final List<Item> items;
    private int total;
    private String description = "";

    A(List<Item> items) {
        this.items = items;
    }

    public void noName(B b, int quantity) {
        b.makeNewSale();
        for (Item item : getItems()) {
            SaleLine line = b.enterItem(item.getID(), quantity);
            total = total + line.total;
            description = line.description;
        }
        b.endSale();
    }

    private List<Item> getItems() {
        return items;
    }
}
#include <string>
#include <vector>

class Item {
public:
    int getID() const { return 0; }
};

struct SaleLine {
    std::string description;
    int total;
};

class B {
public:
    void makeNewSale() { }

    SaleLine enterItem(int itemId, int quantity) {
        return {"", 0};
    }

    void endSale() { }
};

class A {
public:
    explicit A(std::vector<Item> items) : items(items) { }

    void noName(B& b, int quantity) {
        b.makeNewSale();
        for (const Item& item : getItems()) {
            SaleLine line = b.enterItem(item.getID(), quantity);
            total = total + line.total;
            description = line.description;
        }
        b.endSale();
    }

private:
    const std::vector<Item>& getItems() const {
        return items;
    }

    std::vector<Item> items;
    int total = 0;
    std::string description;
};
from dataclasses import dataclass


class Item:
    def get_id(self) -> int:
        return 0


@dataclass
class SaleLine:
    description: str
    total: int


class B:
    def make_new_sale(self) -> None:
        pass

    def enter_item(self, item_id: int, quantity: int) -> SaleLine:
        return SaleLine(description="", total=0)

    def end_sale(self) -> None:
        pass


class A:
    def __init__(self, items: list[Item]) -> None:
        self._items = items
        self._total = 0
        self._description = ""

    def no_name(self, b: B, quantity: int) -> None:
        b.make_new_sale()
        for item in self._get_items():
            line = b.enter_item(item.get_id(), quantity)
            self._total = self._total + line.total
            self._description = line.description
        b.end_sale()

    def _get_items(self) -> list[Item]:
        return self._items
class Item {
  getID(): number {
    return 0;
  }
}

type SaleLine = {
  description: string;
  total: number;
};

class B {
  makeNewSale(): void { }

  enterItem(itemId: number, quantity: number): SaleLine {
    return { description: "", total: 0 };
  }

  endSale(): void { }
}

class A {
  private total = 0;
  private description = "";

  constructor(private readonly items: Item[]) { }

  noName(b: B, quantity: number): void {
    b.makeNewSale();
    for (const item of this.getItems()) {
      const line = b.enterItem(item.getID(), quantity);
      this.total = this.total + line.total;
      this.description = line.description;
    }
    b.endSale();
  }

  private getItems(): Item[] {
    return this.items;
  }
}
LOOP makeNewSale() enterItem(itemID, quantity) description, total endSale() a: A b: B [more items]

The for loop in code maps directly to a loop fragment. The guard condition [more items] is a Boolean expression that describes when the loop continues.

Example 3: Alt Fragment to Code

Given this sequence diagram:

ALT doX(x) calculate() calculate() a: A b: B c: C [x < 10] [else]

Equivalent code in four languages:

class A {
    private final B b;
    private final C c;

    A(B b, C c) {
        this.b = b;
        this.c = c;
    }

    public void doX(int x) {
        if (x < 10) {
            b.calculate();
        } else {
            c.calculate();
        }
    }
}

class B {
    void calculate() { }
}

class C {
    void calculate() { }
}
class B {
public:
    void calculate() { }
};

class C {
public:
    void calculate() { }
};

class A {
public:
    A(B& b, C& c) : b(b), c(c) { }

    void doX(int x) {
        if (x < 10) {
            b.calculate();
        } else {
            c.calculate();
        }
    }

private:
    B& b;
    C& c;
};
class B:
    def calculate(self) -> None:
        pass


class C:
    def calculate(self) -> None:
        pass


class A:
    def __init__(self, b: B, c: C) -> None:
        self._b = b
        self._c = c

    def do_x(self, x: int) -> None:
        if x < 10:
            self._b.calculate()
        else:
            self._c.calculate()
class B {
  calculate(): void { }
}

class C {
  calculate(): void { }
}

class A {
  constructor(
    private readonly b: B,
    private readonly c: C,
  ) { }

  doX(x: number): void {
    if (x < 10) {
      this.b.calculate();
    } else {
      this.c.calculate();
    }
  }
}

Concept Check (Generation): Try translating this code into a sequence diagram before checking the answer:

public class OrderProcessor {
    public void process(Order order, Inventory inv) {
        if (inv.checkStock(order.getItemId())) {
            inv.reserve(order.getItemId());
            order.confirm();
        } else {
            order.reject("Out of stock");
        }
    }
}
Reveal Answer
ALT checkStock(itemId) inStock reserve(itemId) confirm() reject("Out of stock") proc: OrderProcessor inv: Inventory order: Order [inStock == true] [inStock == false]

Real-World Examples

These examples show sequence diagrams for real systems. For each diagram, trace through the arrows top-to-bottom and narrate what is happening before reading the walkthrough.


Example 1: Google Sign-In — OAuth2 Login Flow

Scenario: When you click “Sign in with Google,” three systems exchange a precise sequence of messages. This diagram shows that flow — it illustrates how return messages carry data back and why the ordering of messages matters.

GET /login 302 redirect to accounts.google.com GET /authorize (clientId, scope) 200 auth form POST /authorize (credentials) 302 redirect with authCode GET /callback?code=authCode POST /token (authCode, clientSecret) accessToken 200 session cookie B: Browser A: AppBackend G: GoogleOAuth

What the UML notation captures:

  1. Three lifelines, one flow: Browser, AppBackend, and GoogleOAuth are the three participants. The browser intermediates between your app and Google — this is why OAuth feels like a redirect chain.
  2. Solid arrows (synchronous calls): Every -> means the sender blocks and waits for a response before continuing. The browser sends a request and waits for the redirect before proceeding.
  3. Dashed arrows (return messages): The --> arrows carry responses back — the auth code, the access token, the session cookie. Return messages always flow back to the caller.
  4. Top-to-bottom = time: Reading vertically, you reconstruct the complete OAuth handshake in order. Swapping any two messages would break the protocol — the diagram makes those ordering dependencies visible.

Example 2: DoorDash — Placing a Food Order

Scenario: When a user submits an order, the app charges their card and notifies the restaurant. But what if the payment fails? This diagram uses an alt fragment to model both the success and failure paths explicitly.

ALT submitOrder(items, paymentInfo) charge(amount, card) transactionId notifyNewOrder(items) estimatedTime confirmed(orderId, eta) charge(amount, card) declineReason error(declineReason) app: MobileApp os: OrderService pg: PaymentGateway rest: Restaurant [payment approved] [payment declined]

What the UML notation captures:

  1. alt fragment (if/else): The dashed horizontal line inside the box divides the two branches. Only one branch executes at runtime. When you see alt, think if/else.
  2. Guard conditions in [ ]: [payment approved] and [payment declined] are boolean guards — they must be mutually exclusive so exactly one branch fires.
  3. Different paths, different participants: In the success branch, the flow continues to Restaurant. In the failure branch, it returns immediately to the app. The diagram makes both paths equally visible — no “happy path bias.”
  4. Why alt and not opt? An opt fragment has only one branch (if, no else). Because we have two explicit outcomes — success and failure — alt is the correct choice.

Example 3: GitHub Actions — CI/CD Pipeline Trigger

Scenario: A developer pushes code, GitHub triggers a build, tests run, and deployment happens only if tests pass. This diagram uses opt for conditional deployment and a self-call for internal processing.

OPT git push origin main triggerBuild(commitSha) runTests() testResults deployToStaging(artifact) stagingUrl notify(testResults) dev: Developer gh: GitHub build: BuildService deploy: DeployService [all tests passed]

What the UML notation captures:

  1. Self-call (build -> build): A message from a lifeline back to itself models an internal call — BuildService running its own test suite. The arrow loops back to the same column.
  2. opt fragment (if, no else): Deployment only happens if all tests pass. There is no “else” branch — on failure the flow skips the opt block and continues to the notification.
  3. Return after the fragment: gh --> dev: notify(testResults) executes regardless of whether deployment occurred — it is outside the opt box, at the outer sequence level.
  4. Activation ordering: build runs runTests() before returning testResults to gh. Top-to-bottom ordering guarantees tests complete before GitHub is notified.

Example 4: Uber — Real-Time Driver Matching

Scenario: When a rider requests a trip, the matching service offers the ride to drivers until one accepts. This diagram shows a loop fragment combined with an alt inside — the most powerful combination in sequence diagrams.

LOOP ALT requestRide(location, rideType) offerRide(request) accepted offerRide(request) declined notifyRider(driverId, eta) driverAssigned(eta) rider: RiderApp match: MatchingService driver: DriverApp notif: NotificationService [no driver accepted] [driver accepts] [driver declines or timeout]

What the UML notation captures:

  1. loop fragment: The matching service repeats the offer-cycle until a driver accepts. loop models iteration — equivalent to a while loop. In practice this loop has a timeout (e.g., 3 attempts before cancellation), which would be the loop guard condition.
  2. Nested alt inside loop: Each iteration of the loop has its own if/else: did the driver accept or decline? Nesting fragments is valid and common — it directly mirrors nested control flow in code.
  3. Flow continues after the loop: Once a driver accepts, execution exits the loop and the notification is sent. Messages outside a fragment are unconditional.
  4. DriverApp as a participant: The driver’s mobile app is a first-class lifeline. This shows that sequence diagrams can include mobile clients, web clients, and backend services on equal footing.

Example 5: Slack — Real-Time Message Delivery

Scenario: When you send a Slack message, it is persisted, then broadcast to all subscribers of that channel. This diagram shows the fan-out delivery pattern using a loop fragment.

LOOP sendMessage(channelId, text) persist(channelId, text, userId) messageId broadcastToChannel(channelId, message) deliver(userId, message) messageReceived ack(messageId) client: SlackClient ws: WebSocketGateway msg: MessageService notif: NotificationService [for each online subscriber]

What the UML notation captures:

  1. Sequence before the loop: persist and get messageId happen exactly once — before the broadcast. The diagram makes this ordering explicit: a message is saved before it is delivered to anyone.
  2. loop for fan-out delivery: Each online subscriber receives their own delivery call. In a channel with 200 members, the loop body executes 200 times. The diagram abstracts this into a single readable fragment.
  3. ack after the loop: The sender receives their acknowledgment (ack(messageId)) only after the broadcast completes. This is outside the loop — it is unconditional and happens once.
  4. WebSocketGateway as the central hub: All messages flow in and out through the gateway. The diagram shows this hub topology clearly — every arrow touches ws, revealing it as the architectural bottleneck. This is a useful architectural insight visible only in the sequence diagram.

Chapter Summary

Sequence diagrams are a powerful tool to understand the dynamic, time-based behavior of a system.

  • Lifelines and Messages establish the basic timeline of communication.
  • OPT fragments handle “maybe” scenarios (if).
  • ALT fragments handle “either/or” scenarios (if/else).
  • LOOP fragments handle repetitive scenarios (while/for).

By mastering these fragments, you can model nearly any procedural logic within an object-oriented system before writing a single line of code.

End of Chapter Exercises (Retrieval Practice)

To solidify your learning, attempt these questions without looking back at the text.

  1. What is the key difference between an ALT fragment and an OPT fragment?
  2. If you needed to model a user trying to enter a password 3 times before being locked out, which fragment would you use as the outer box, and which fragment would you use inside it?
  3. Draw a simple sequence diagram (using pen and paper) of yourself ordering a book online. Include one OPT fragment representing applying a promo code.

Interactive Practice

Test your knowledge with these retrieval practice exercises. These diagrams are rendered dynamically to ensure you can recognize UML notation in any context.

Knowledge Quiz

UML Sequence Diagram Practice

Test your ability to read and interpret UML Sequence Diagrams.

What type of message is represented by a solid line with a filled (solid) arrowhead?

request() a: Client b: Server
Correct Answer:

What does the dashed line in the diagram below represent?

calculate() result a: Client b: Server
Correct Answer:

Which combined fragment would you use to model an if-else decision in a sequence diagram?

ALT login(user, pass) token error c: Client a: AuthService [credentials valid] [credentials invalid]
Correct Answer:

Look at this diagram. How many times could the ping() message be sent?

LOOP connect() ping() ack() app: App server: Server [1, 5]
Correct Answer:

Which of the following are valid combined fragment types in UML sequence diagrams? (Select all that apply.)

Correct Answers:

What does the opt fragment in this diagram mean?

OPT calculateTotal() applyDiscount() discountApplied() finalTotal() c: Checkout p: Pricing Engine [hasPromoCode == true]
Correct Answer:

In UML sequence diagrams, what does time represent?

Correct Answer:

Which arrow style represents an asynchronous message where the sender does NOT wait for a response?

Correct Answer:

What does an activation bar (thin rectangle on a lifeline) represent?

placeOrder(items) saveOrder(items) orderId confirmation(orderId) ui: UI os: OrderService db: Database
Correct Answer:

What is the correct lifeline label format for an unnamed instance of class ShoppingCart?

submit() receipt sc: ShoppingCart ch: Checkout
Correct Answer:

Given this Java code, which sequence diagram element represents the new Payment(amount) call? java public void makePayment(int amount) { Payment p = new Payment(amount); p.authorize(); }

p: Payment <<create>> authorize() authorized ch: Checkout
Correct Answer:

A sequence diagram and a class diagram are drawn for the same system. An arrow in the sequence diagram shows order -> inventory: checkStock(itemId). What must be true in the class diagram?

Correct Answer:

Retrieval Flashcards

UML Sequence Diagram Flashcards

Quick review of UML Sequence Diagram notation and fragments.

What is the difference between a synchronous and an asynchronous message arrow?

How is a return message drawn in a sequence diagram?

What is the difference between an opt fragment and an alt fragment?

What does a lifeline represent, and how is it drawn?

Name the combined fragment you would use to model a for/while loop in a sequence diagram.

What does an activation bar (execution specification) represent on a lifeline?

What is the correct naming convention for lifelines in sequence diagrams?

What is the par combined fragment used for?

Pedagogical Tip: If you find these challenging, it’s a good sign! Effortful retrieval is exactly what builds durable mental models. Try coming back to these tomorrow to benefit from spacing and interleaving.

Interactive Tutorials

Master UML sequence diagrams by writing code that matches target diagrams in our interactive tutorials: