CS 130


Welcome to Computer Science 130 - Software Engineering at UCLA

Requirements


Requirements define the problem space. They capture what the system must do and what the user actually needs to achieve. We care about them for several key reasons:

  • Defining “Correctness”: A requirement establishes the exact criteria for whether an implementation is successful. Without clear requirements, developers have no objective way to know when a feature is “done” or if it actually works as intended.
  • Building the Right System: You can write perfectly clean, highly optimized, bug-free code—but if it doesn’t solve the user’s actual problem, the software is useless. Requirements ensure the engineering team’s efforts are aligned with user value.
  • Traceability and Testing: Good requirements allow developers to write clear acceptance criteria and enable traceability – the ability to link implemented features back to the requirements that motivated them. This supports impact analysis when requirements change and helps verify that the system delivers what was requested.

Requirements vs. Design

In software engineering, distinguishing between requirements and design is critical to building successful systems. Requirements express what the system should do and capture the user’s needs. The goal of requirements, in general, is to capture the exact set of criteria that determine if an implementation is “correct”.

A design, on the other hand, describes how the system implements these user needs. Design is about exploring the space of possible solutions to fulfill the requirements. A well-crafted requirements specification should never artificially limit this space by prematurely making design decisions. For example, a requirement for pathfinding might be: “The program should find the shortest path between A and B”. If you were to specify that “The program should implement Dijkstra’s shortest path algorithm”, you would over-constrain the system and dictate a design choice before development even begins.

Examples

Here are some examples illustrating the difference between a requirement (what the system must do to satisfy the user’s needs) and a design decision (how the engineers choose to implement a solution to fulfill that requirement):

  • Route Planning
    • Requirement: The system must calculate and display the shortest route between a user’s current location and their destination.
    • Design Decision: Implement Dijkstra’s algorithm (or A* search) to calculate the path, representing the map as a weighted graph.
  • User Authentication
    • Requirement: The system must ensure that only registered and verified users can access the financial dashboard.
    • Design Decision: Use OAuth 2.0 for third-party login and issue JSON Web Tokens (JWT) to manage user sessions.
  • Data Persistence
    • Requirement: The application must save a user’s shopping cart items so they are not lost if the user accidentally closes their browser.
    • Design Decision: Store the active shopping cart data temporarily in a Redis in-memory data store for fast retrieval, rather than saving it to the main relational database.
  • Sorting Information
    • Requirement: The system must display the list of available university courses ordered alphabetically by their course name.
    • Design Decision: Use the built-in TimSort algorithm in Python to sort the array of course objects before sending the data to the frontend.
  • Cross-Platform Accessibility
    • Requirement: The web interface must be fully readable and navigable on both large desktop monitors and small mobile phone screens.
    • Design Decision: Build the user interface using React.js and apply Tailwind CSS to create a responsive, mobile-first grid layout.
  • Search Functionality
    • Requirement: Users must be able to search for specific books in the catalog using keywords, titles, or author names, even if they make minor typos.
    • Design Decision: Integrate Elasticsearch to index the book catalog and utilize its fuzzy matching capabilities to handle user typos.
  • System Communication
    • Requirement: When a customer places an order, the inventory system must be notified to reduce the stock count of the purchased items.
    • Design Decision: Implement an event-driven architecture using an Apache Kafka message broker to publish an “OrderPlaced” event that the inventory service listens for.
  • Password Security
    • Requirement: The system must securely store user passwords so that even if the database is compromised, the original passwords cannot be easily read.
    • Design Decision: Hash all passwords using the bcrypt algorithm with a work factor (salt) of 12 before saving them to the database.
  • Real-Time Collaboration
    • Requirement: Multiple users must be able to view and edit the same code file simultaneously, seeing each other’s changes in real-time without refreshing the page.
    • Design Decision: Establish a persistent two-way connection between the clients and the server using WebSockets, and use Operational Transformation (OT) to resolve edit conflicts.
  • Offline Capabilities
    • Requirement: The mobile app must allow users to read previously opened news articles even when they lose internet connection (e.g., when entering a subway).
    • Design Decision: Cache the text and images of recently opened articles locally on the device using an SQLite database embedded in the mobile application.

Why Does the Difference Matter?

Blurring the lines between requirements and design is a common mistake that leads to misunderstandings. In practice, the two are often pursued cooperatively and contemporaneously, yet the distinction matters for three main reasons:

Avoiding Premature Constraints: When you put design decisions into your requirements, you artificially limit the space of possible solutions before development even begins. If a product manager writes a requirement that says, “The system must use an SQL database to store user profiles”, they have made a design decision. A NoSQL database or an in-memory cache might have been vastly superior for this specific use case, but the engineers are now blocked from exploring those better options.

Preserving Flexibility and Agility: Design decisions change frequently. A team might start by using one sorting algorithm or database architecture, realize it doesn’t scale well, and swap it out for another. If the requirement was strictly about the “what” (e.g., “Data must be sorted alphabetically”), the requirement stays the same even when the design changes. This iterative process of swinging between requirements and design helps manage the complexity of what Rittel and Webber termed “wicked” problems (Rittel and Webber 1973) – problems where understanding the requirements depends on exploring the solution. If the design was baked into the requirement, you now have to rewrite your requirements and change your acceptance criteria just to fix a technical issue.

Utilizing the Right Expertise: Requirements are typically driven by the customer or product manager / product owner — the people who understand the business needs. Design decisions are typically led by the software engineers and architects — the people who understand the technology. However, effective teams involve users in design validation (through prototyping and user testing) and engineers in requirements discovery (since technical possibilities shape what can be offered). Mixing the two without clear awareness often results in non-technical stakeholders dictating technical implementations, which rarely ends well.

In short: Requirements keep you focused on delivering value to the user. Leaving design out of your requirements empowers your engineers to deliver that value in the most efficient and technically sound way possible.

Requirements Specifications

User Stories

Quality Attribute Scenarios

Quality attribute requirements (such as performance, security, and availability) are often best captured via “Quality Attribute Scenarios” to make them concrete and measurable (Bass et al. 2012).

Formal Requirements Specifications

Requirements Elicitation

Software Requirements Quiz

Recalling what you just learned is the best way to form lasting memory. Use this quiz to test your ability to discriminate between problem-space statements (requirements) and solution-space statements (design) in novel scenarios.

Difficulty: Intermediate

A startup is building a new music streaming application. The product owner states, ‘Listeners need the ability to seamlessly transition between songs without any perceivable loading delays.’ What does this statement best represent?

Correct Answer:
Difficulty: Intermediate

A Quality Assurance (QA) engineer is writing automated checks for a new e-commerce checkout flow. They ensure that every test maps directly back to a specific stakeholder request. Which core benefit of defining the problem space does this mapping best demonstrate?

Correct Answer:
Difficulty: Advanced

A client requests a new social media dashboard and specifies, ‘The platform must use a graph database to map user connections.’ Why might a software architect push back on this specific phrasing?

Correct Answer:
Difficulty: Basic

In a cross-functional Agile team, who is ideally suited to articulate the functional expectations of a new feature, and who should decide the underlying technical mechanics?

Correct Answer:
Difficulty: Intermediate

Which of the following statements represents an exploration of the solution space rather than a statement of user need?

Correct Answer:
Difficulty: Intermediate

A development team originally built a search feature using a basic database query but later migrated to a dedicated indexing engine to handle typos more effectively. If their original specification was written perfectly, what happened to that specification during this technical migration?

Correct Answer:
Difficulty: Basic

A team needs to ensure their new banking portal can handle 10,000 simultaneous logins within two seconds without crashing. What is the recommended format for capturing this specific type of system characteristic?

Correct Answer:
Difficulty: Basic

A transit application needs to serve commuters who frequently lose cell service in subway tunnels. Which of the following represents the ‘how’ (the implementation) rather than the ‘what’ for this scenario?

Correct Answer:

User Stories


User stories are the most commonly used format to specify requirements in a light-weight, informal way (particularly in projects following Agile processes). Each user story is a high-level description of a software feature written from the perspective of the end-user.

User stories act as placeholders for a conversation between the technical team and the “business” side to ensure both parties understand the why and what of a feature.

Format

User stories follow this format:


As a [user role],

I want [to perform an action]

so that [I can achieve a goal]


For example:

(Smart Grocery Application): As a home cook, I want to swap out ingredients in a recipe so that I can accommodate my dietary restrictions and utilize what I already have in my kitchen.

(Travel Itinerary Planner): As a frequent traveler, I want to discover unique, locally hosted activities so that I can experience the authentic culture of my destination rather than just the standard tourist traps.

This structure helps the team identify not just the “what”, but also the “who” and — most importantly — the “why”.

The main requirement of the user story is captured in the I want part. The so that part primarily clarifies the goal the user wants to achieve. While it should not prescribe implementation details, it may implicitly introduce quality constraints or dependencies that shape the acceptance criteria.

Be specific about the actor. Avoid generic labels like “user” in the As a clause. Instead, name the specific role that benefits from the feature (e.g., “job seeker”, “hiring manager”, “store owner”). A precise actor clarifies who needs the feature and why, helps the team understand the context, and prevents stories from becoming vague catch-alls. If you find yourself writing “As a user”, ask: which user?

Acceptance Criteria

While the story itself is informal, we make it actionable using Acceptance Criteria. They define the boundaries of the feature and act as a checklist to determine if a story is “done”. Acceptance criteria define the scope of a user story.

They follow this format:


Given [pre-condition / initial state]

When [action]

Then [post-condition / outcome]


For example:

(Smart Grocery Application): As a home cook, I want to swap out ingredients in a recipe so that I can accommodate my dietary restrictions and utilize what I already have in my kitchen.

  • Given the user is viewing a recipe’s ingredient list, when they select a specific ingredient, then a list of viable alternatives should be suggested.
  • Given the user selects a substitute from the alternatives list, when they confirm the swap, then the recipe’s required quantities and nutritional estimates should recalculate and update on the screen.
  • Given the user has modified a recipe with substitutions, when they save it to their cookbook, then the customized version of the recipe should be stored in their personal profile without altering the original public recipe.

These acceptance criteria add clarity to the user story by defining the specific conditions under which the feature should work as expected. They also help to identify potential edge cases and constraints that need to be considered during development. The acceptance criteria define the scope of conditions that check whether an implementation is “correct” and meets the user’s needs. So naturally, acceptance criteria must be specific enough to be testable but should not be overly prescriptive about the implementation details, not to constrain the developers more than really needed to describe the true user need.

Here is another example:

(Travel Itinerary Planner): As a frequent traveler, I want to discover unique, locally hosted activities so that I can experience the authentic culture of my destination rather than just the standard tourist traps.

  • Given the user has set their upcoming trip destination to a city, when they browse local experiences, then they should see a list of activities hosted by verified local residents.
  • Given the user is browsing the experiences list, when they filter by a maximum budget of $50, then only activities within that price range should be shown.
  • Given the user selects a specific local experience, when they check availability, then open booking slots for their specific travel dates should be displayed.

INVEST

To evaluate if a user story is well-written, we apply the INVEST criteria:

  • Independent: Stories should not depend on each other so they can be implemented and released in any order.
  • Negotiable: They capture the essence of a need without dictating specific design decisions (like which database to use).
  • Valuable: The feature must deliver actual benefit to the user, not just the developer.
  • Estimable: The scope must be clear enough for developers to predict the effort required.
  • Small: A story should be small enough that the team can complete it within a single iteration and estimate it with reasonable confidence.
  • Testable: It must be verifiable through its acceptance criteria.

Important: The application of the INVEST criteria is often content-dependent. For example, a story that is quite large to implement but cannot be effectively split into separate user stories can still be considered “small enough” while a user story that is objectively faster and easier to implement can be considered “not small” if splitting it up into separate user stories that are still valuable and independent is more elegant. Or a user story that is “independent” in one set of user stories (because all its dependencies have already been implemented) is “not independent” if it is in a set of user stories where its dependencies have not been implemented yet and therefore a dependency is still in the user story set. Understanding this crucial aspect of the INVEST criteria is key to evaluating user stories.

We will now look at these criteria in more detail below.

Independent

An independent story does not overlap with or depend on other stories—it can be scheduled and implemented in any order.

What it is and Why it Matters The “Independent” criterion states that user stories should not overlap in concept and should be schedulable and implementable in any order (Wake 2003). An independent story can be understood, tracked, implemented, and tested on its own, without requiring other stories to be completed first.

This criterion matters for several fundamental reasons:

  • Flexible Prioritization: Independent stories allow the business to prioritize the backlog based strictly on value, rather than being constrained by technical dependencies (Wake 2003). Without independence, a high-priority story might be blocked by a low-priority one.
  • Accurate Estimation: When stories overlap or depend on each other, their estimates become entangled. For example, if paying by Visa and paying by MasterCard are separate stories, the first one implemented bears the infrastructure cost, making the second one much cheaper (Cohn 2004). This skews estimates.
  • Reduced Confusion: By avoiding overlap, independent stories reduce places where descriptions contradict each other and make it easier to verify that all needed functionality has been described (Wake 2003).

How to Evaluate It To determine if a user story is independent, ask:

  1. Does this story overlap with another story? If two stories share underlying capabilities (e.g., both involve “sending a message”), they have overlap dependency—the most painful form (Wake 2003).
  2. Must this story be implemented before or after another? If so, there is an order dependency. While less harmful than overlap (the business often naturally schedules these correctly), it still constrains planning (Wake 2003).
  3. Was this story split along technical boundaries? If one story covers the UI layer and another covers the database layer for the same feature, they are interdependent and neither delivers value alone (Cohn 2004).

How to Improve It If stories violate the Independent criterion, you can improve them using these techniques:

  • Combine Interdependent Stories: If two stories are too entangled to estimate separately, merge them into a single story. For example, instead of separate stories for Visa, MasterCard, and American Express payments, combine them: “A company can pay for a job posting with a credit card” (Cohn 2004).
  • Partition Along Different Dimensions: If combining makes the story too large, re-split along a different dimension. For overlapping email stories like “Team member sends and receives messages” and “Team member sends and replies to messages”, repartition by action: “Team member sends message”, “Team member receives message”, “Team member replies to message” (Wake 2003).
  • Slice Vertically: When stories have been split along technical layers (UI vs. database), re-slice them as vertical “slices of cake” that cut through all layers. Instead of “Job Seeker fills out a resume form” and “Resume data is written to the database”, write “Job Seeker can submit a resume with basic information” (Cohn 2004).

Examples of Stories Violating the Independent Criterion

Example 1: Overlap Dependency

Story A: As a team member, I want to send and receive messages so that I can communicate with my colleagues.”

  • Given I am on the messaging page, When I compose a message and click “Send”, Then the message appears in the recipient’s inbox.
  • Given a colleague has sent me a message, When I open my inbox, Then I can read the message.

Story B: As a team member, I want to reply to messages so that I can indicate which message I am responding to.”

  • Given I have received a message, When I click the “Reply” button and submit my response, Then the reply is sent to the original sender.
  • Given the reply has been received, When the original sender views the message, Then it is displayed as a reply to the original message.
  • Negotiable: Yes. Neither story dictates a specific UI or technology.
  • Valuable: Yes. Communication features are clearly valuable to users.
  • Estimable: Difficult. Because both stories share the “send” capability, whichever story is implemented second has unpredictable effort—parts of it may already be done, making estimates unreliable.
  • Small: Yes. Each story is a manageable chunk of work that fits within a sprint.
  • Testable: Yes. Clear acceptance criteria can be written for sending, receiving, and replying.
  • Why it violates Independent: Both stories include “sending a message”—this is an overlap dependency, the most harmful form of story dependency (Wake 2003). If Story A is implemented first, parts of Story B are already done. If Story B is implemented first, parts of Story A are already done. This creates confusion about what is covered and makes estimation unreliable.
  • How to fix it: Make the dependency explicit (e.g., User story B depends on user story A). Merging them into one story is not an option as it would violate the small criterion, splitting them into three stories (sending, receiving and replying) is not an option as it would still violate the independent criterion and also violate valuable for just sending without receiving. So the best thing we can do is to accept that we cannot always create perfectly independent user stories and instead document this dependency so that when scheduling the implementation of user stories we can directly see that they have to be implemented in a specific order and when estimating user stories we can assume that the functionality in user story A has already been implemented. Hidden dependencies are bad. Full independence is perfect but not always achievable. Explicit dependencies are the pragmatic workaround that addresses the core problem of hidden dependencies while still acknowledging practicality.

Example 2: Technical (Horizontal) Splitting

Story A: As a job seeker, I want to fill out a resume form so that I can enter my information.”

  • Given I am on the resume page, When I fill in my name, address, and education, Then the form displays my entered information.

Story B: As a job seeker, I want my resume data to be saved so that it is available when I return.”

  • Given I have filled out the resume form, When I click “Save”, Then my resume data is available when I log back in.
  • Negotiable: Yes. Neither story mandates a specific technology, database, or framework—the implementation details are open to discussion.
  • Valuable: No. Neither story delivers value on its own—a form that does not save is useless, and saving data without a form to collect it is equally useless.
  • Estimable: Yes. Developers can estimate each technical task.
  • Small: Yes. Each is a small piece of work.
  • Testable: Yes, though the horizontal split makes end-to-end testing awkward.
  • Why it violates Independent: Story B is meaningless without Story A, and Story A is useless without Story B. They are completely interdependent because the feature was split along technical boundaries (UI layer vs. persistence layer) instead of user-facing functionality (Cohn 2004).
  • How to fix it: Combine into a single vertical slice: “As a job seeker, I want to submit a resume with basic information (name, address, education) so that employers can find me.” This cuts through all layers and delivers value independently (Cohn 2004).

Quick Check: Consider these two stories for a music streaming app:

  • Story A: As a listener, I want to create playlists so that I can organize my music.”
  • Story B: As a listener, I want to add songs to a playlist so that I can build my collection.”

Are these stories independent? Why or why not?

Reveal Answer They are not independent — they have an order dependency (the less harmful form, compared to overlap dependency) (Wake 2003). Story B requires playlists to exist (Story A). There are two valid approaches: (1) Combine them: "As a listener, I want to create and populate playlists so that I can organize my music." (2) Accept the dependency: Since order dependencies are less harmful than overlap dependencies, the team can keep both stories separate and simply ensure Story A is scheduled first. The business often naturally handles this ordering correctly (Wake 2003).

Negotiable

A negotiable story captures the essence of a user’s need without locking in specific design or technology decisions—the details are worked out collaboratively.

What it is and Why it Matters The “Negotiable” criterion states that a user story is not an explicit contract for features; rather, it captures the essence of a user’s need, leaving the details to be co-created by the customer and the development team during development (Wake 2003). A good story captures the essence, not the details (see also “Requirements Vs. Design”).

This criterion matters for several fundamental reasons:

  • Enabling Collaboration: Because stories are intentionally incomplete, the team is forced to have conversations to fill in the details. Ron Jeffries describes this through the three C’s: Card (the story text), Conversation (the discussion), and Confirmation (the acceptance tests) (Cohn 2004). The card is merely a token promising a future conversation (Wake 2003).
  • Evolutionary Design: High-level stories define capabilities without over-constraining the implementation approach (Wake 2003). This leaves room to evolve the solution from a basic form to an advanced form as the team learns more about the system’s needs.
  • Avoiding False Precision: Including too many details early creates a dangerous illusion of precision (Cohn 2004). It misleads readers into believing the requirement is finalized, which discourages necessary conversations and adaptation.

How to Evaluate It To determine if a user story is negotiable, ask:

  1. Does this story dictate a specific technology or design decision? Words like “MongoDB”, “HTTPS”, “REST API”, or “dropdown menu” in a story are red flags that it has left the space of requirements and entered the space of design.
  2. Could the development team solve this problem using a completely different technology or layout, and would the user still be happy? If the answer is yes, the story is negotiable. If the answer is no, the story is over-constrained.
  3. Does the story include UI details? Embedding user interface specifics (e.g., “a print dialog with a printer list”) introduces premature assumptions before the team fully understands the business goals (Cohn 2004).

How to Improve It If a story violates the Negotiable criterion, you can improve it using these techniques:

  • Focus on the “Why”: Use “So that” clauses to clarify the underlying goal, which allows the team to negotiate the “How”.
  • Specify What, Not How: Replace technology-specific language with the user need it serves. Instead of “use HTTPS”, write “keep data I send and receive confidential”.
  • Define Acceptance Criteria, Not Steps: Define the outcomes that must be true, rather than the specific UI clicks or database queries required.
  • Keep the UI Out as Long as Possible: Avoid embedding interface details into stories early in the project (Cohn 2004). Focus on what the user needs to accomplish, not the specific controls they will use.

Examples of Stories Violating the Negotiable Criterion

Example 1: The Technology-Specific Story

As a subscriber, I want my profile settings saved in a MongoDB database so that they load quickly the next time I log in.”

  • Given I am logged in and I change my profile settings, When I log out and log back in, Then my profile settings are still applied.
  • Independent: Yes. Saving profile settings does not depend on other stories.
  • Valuable: Yes. Remembering user settings is clearly valuable.
  • Estimable: Yes. A developer can estimate the effort to implement settings persistence.
  • Small: Yes. This is a focused piece of work.
  • Testable: Yes. You can verify that settings persist across sessions.
  • Why it violates Negotiable: Specifying “MongoDB” is a design decision. The user does not care where the data lives. The engineering team might realize that a relational SQL database or local browser caching is a much better fit for the application’s architecture.
  • How to fix it: As a subscriber, I want the system to remember my profile settings so that I don’t have to re-enter them every time I log in.”

Example 2: The UI-Specific Story

As a student, I want to select my courses from a dropdown menu so that I can register for the upcoming semester.”

  • Given I am on the registration page, When I select a course from the dropdown menu and click “Register”, Then the course is added to my schedule.
  • Independent: Yes. Course registration does not depend on other stories.
  • Valuable: Yes. Registering for courses is clearly valuable to the student.
  • Estimable: Yes. Building a course selection feature is well-understood work.
  • Small: Yes. This is a single, focused feature.
  • Testable: Yes. You can verify that selecting a course adds it to the schedule.
  • Why it violates Negotiable: “Dropdown menu” is a specific UI design decision. The user’s actual need is to select courses, which could be achieved through many different interfaces—a search bar, a visual schedule builder, a drag-and-drop interface, or even a conversational assistant. By prescribing the dropdown, the story constrains the design team before they have explored the problem space (Cohn 2004).
  • How to fix it: As a student, I want to select courses for the upcoming semester so that I can register for my classes.” Similarly, specifying protocols (e.g., “use HTTPS”), frameworks (e.g., “built with React”), or architectural patterns (e.g., “using microservices”) are all design decisions that constrain the solution space.

Quick Check: As a restaurant owner, I want customers to scan a QR code at their table to view the menu on their phone so that I don’t have to print physical menus.”

Does this story satisfy the Negotiable criterion?

Reveal Answer No. "Scan a QR code" prescribes a specific solution. The owner's actual need is for customers to access the menu without physical copies — this could be achieved via QR codes, NFC tags, a URL, a dedicated app, or a table-mounted tablet. A negotiable version: "As a restaurant owner, I want customers to access the menu digitally at their table so that I can eliminate printed menus."

What to do when the user really needs the specific technology?

Sometimes the required solution does indeed have to conform to the specific technology that the customer is using in their organization. In software engineering we call this a “technical constraint”. In these cases user stories are usually not the ideal format to specify these requirement in, since these technical constraints are often cross-cutting and should be included in the design of many different independent features. User stories are a mechanism to document requirements that primarily concern the functionality of the software. Other kinds of requirements, especially those that can’t be declared “done” should use different kinds of requirements specifications.

Valuable

A valuable story delivers tangible benefit to the customer, purchaser, or user—not just to the development team.

What it is and Why it Matters The “Valuable” criterion states that every user story must deliver tangible value to the customer, purchaser, or user—not just to the development team (Wake 2003). A good story focuses on the external impact of the software in the real world: if we frame stories so their impact is clear, product owners and users can understand what the stories bring and make good prioritization choices (Wake 2003).

This criterion matters for several fundamental reasons:

  • Informed Prioritization: The product owner prioritizes the backlog by weighing each story’s value against its cost. If a story’s business value is opaque—because it is written in technical jargon—the customer cannot make intelligent scheduling decisions (Cohn 2004).
  • Avoiding Waste: Stories that serve only the development team (e.g., refactoring for its own sake, adopting a trendy technology) consume iteration capacity without moving the product closer to its users’ goals. The IRACIS framework provides a useful lens for value: does the story Increase Revenue, Avoid Costs, or Improve Service? (Wake 2003)
  • User vs. Purchaser Value: It is tempting to say every story must be valued by end-users, but that is not always correct. In enterprise environments, the purchaser may value stories that end-users do not care about (e.g., “All configuration is read from a central location” matters to the IT department managing 5,000 machines, not to daily users) (Cohn 2004).

How to Evaluate It To determine if a user story is valuable, ask:

  1. Would the customer or user care if this story were dropped? If only developers would notice, the story likely lacks user-facing value.
  2. Can the customer prioritize this story against others? If the story is written in “techno-speak” (e.g., “All connections go through a connection pool”), the customer cannot weigh its importance (Cohn 2004).
  3. Does this story describe an external effect or an internal implementation detail? Valuable stories describe what happens on the edge of the system—the effects of the software in the world—not how the system is built internally (Wake 2003).

How to Improve It If stories violate the Valuable criterion, you can improve them using these techniques:

  • Rewrite for External Impact: Translate the technical requirement into a statement of benefit for the user. Instead of “All connections to the database are through a connection pool”, write “Up to fifty users should be able to use the application with a five-user database license” (Cohn 2004).
  • Let the Customer Write: The most effective way to ensure a story is valuable is to have the customer write it in the language of the business, rather than in technical jargon (Cohn 2004).
  • Focus on the “So That”: A well-written “so that” clause forces the author to articulate the real-world benefit. If you cannot complete “so that [some user benefit]” without referencing technology, the story is likely not valuable.
  • Complete the Acceptance Criteria: A story may appear valuable but have incomplete acceptance criteria that leave out essential functionality, effectively making the delivered feature useless.

Examples of Stories Violating the Valuable Criterion

Example 1: Incomplete Acceptance Criteria That Miss the Value

As a travel agent, I want to search for available flights for a client’s trip so that I can find the best option for them.”

  • Given the travel agent enters a departure city, destination city, and travel date, When they click “Search”, Then a list of available flights for that route is displayed.
  • Given the search results are displayed, When the travel agent selects a flight from the list, Then the booking page for that flight is shown.
  • Independent: Yes. Searching for flights does not depend on other stories.
  • Negotiable: Yes. The story does not prescribe any specific technology, UI layout, or data source—the team is free to decide how to build the search.
  • Estimable: Yes. Building a flight search with results display is well-understood work with clear scope.
  • Small: Yes. A single search-and-display feature fits within a sprint.
  • Testable: Yes. The given acceptance criteria can be translated into an unambiguous test with concrete steps and clear testing criteria.
  • Why it violates Valuable: The story text promises real value (“find the best option”), but the acceptance criteria do not mention it. Since acceptance criteria define the scope of an acceptance implementation to the user story, these acceptance criteria accept user stories that do not implement the main functionality. A list of flight names and times is useless to a travel agent who needs to compare prices, layover durations, and total travel time to recommend the best option to a client. Without this comparison data, the agent cannot accomplish the goal stated in the “so that” clause. The feature technically works—flights are displayed and can be selected—but it does not solve the user’s actual problem. This illustrates why acceptance criteria must capture the essential functionality that delivers the value promised by the story. A story may appear valuable based on its text, but if its acceptance criteria leave out the information or capability that makes the feature genuinely useful, the delivered feature might not provide real value to the user. In this example, the acceptance criteria should help the developers understand what information is needed for the user to find the best option. Since the developers could pick any random subset of attributes their selection might not be what the user really needs to see. So our acceptance criteria should clearly communicate what it is the user really needs.
  • How to fix it: Add acceptance criteria that capture the comparison capability essential to the agent’s real goal: Given the search results are displayed, When the travel agent views the list, Then each flight shows the ticket price, number of stops, layover durations, and total travel time so the agent can compare options side by side.”

Quick Check: As a backend developer, I want to migrate our logging from printf statements to a structured logging framework so that log entries are in JSON format.”

Does this story satisfy the Valuable criterion?

Reveal Answer No. While this story might make it easier for developers to deliver more value to the user in the future due to better maintainability, it does not directly deliver value to a user of the system. We consider a user story valuable only if it meets the need of a user.

Example 2: The Developer-Centric Story

As a developer, I want to refactor the authentication module so that the codebase is easier to maintain.”

  • Given the authentication module has been refactored, When a developer deploys the updated module, Then all existing authentication endpoints return identical responses.
  • Independent: Yes. Refactoring the auth module does not depend on other stories.
  • Negotiable: Yes. The story does not dictate a specific technology, language, or design decision—the team is free to choose how to improve maintainability.
  • Estimable: Yes. A developer can estimate the effort of a refactoring task.
  • Small: Yes. Refactoring a single module can fit within a sprint.
  • Testable: Yes. You can verify the refactored module passes all existing authentication tests.
  • Why it violates Valuable: The story is written entirely from the developer’s perspective. The user does not care about internal code quality. The “so that” clause (“the codebase is easier to maintain”) describes a developer benefit, not a user benefit (Cohn 2004). A product owner cannot weigh “easier to maintain” against user-facing features.
  • How to fix it: If there is a legitimate user-facing reason (e.g., performance), rewrite the story around that benefit: As a registered member, I want to log in without noticeable delay so that I can start using the application immediately.”

Estimable

An estimable story has a scope clear enough for the development team to make a reasonable judgment about the effort required.

What it is and Why it Matters The “Estimable” criterion states that the development team must be able to make a reasonable judgment about a story’s size, cost, or time to deliver (Wake 2003). While precision is not the goal, the estimate must be useful enough for the product owner to prioritize the story against other work (Cohn 2004).

This criterion matters for several fundamental reasons:

  • Enabling Prioritization: The product owner ranks stories by comparing value to cost. If a story cannot be estimated, the cost side of this equation is unknown, making informed prioritization impossible (Cohn 2004).
  • Supporting Planning: Stories that cannot be estimated cannot be reliably scheduled into an iteration. Without sizing information, the team risks committing to more (or less) work than they can deliver.
  • Surfacing Unknowns Early: An unestimable story is a signal that something important is not understood—either the domain, the technology, or the scope. Recognizing this early prevents costly surprises later.

How to Evaluate It Developers generally cannot estimate a story for one of three reasons (Cohn 2004):

  1. Lack of Domain Knowledge: The developers do not understand the business context. For example, a story saying “New users are given a diabetic screening” could mean a simple web questionnaire or an at-home physical testing kit—without clarification, no estimate is possible (Cohn 2004).
  2. Lack of Technical Knowledge: The team understands the requirement but has never worked with the required technology. For example, a team asked to expose a gRPC API when no one has experience with Protocol Buffers or gRPC cannot estimate the work (Cohn 2004).
  3. The Story is Too Big: An epic like “A job seeker can find a job” encompasses so many sub-tasks and unknowns that it cannot be meaningfully sized as a single unit (Cohn 2004).

How to Improve It The approach to fixing an unestimable story depends on which barrier is blocking estimation:

  • Conversation (for Domain Knowledge Gaps): Have the developers discuss the story directly with the customer. A brief conversation often reveals that the requirement is simpler (or more complex) than assumed, making estimation possible (Cohn 2004).
  • Spike (for Technical Knowledge Gaps): Split the story into two: an investigative spike—a brief, time-boxed experiment to learn about the unknown technology—and the actual implementation story. The spike itself is always given a defined maximum time (e.g., “Spend exactly two days investigating credit card processing”), which makes it estimable. Once the spike is complete, the team has enough knowledge to estimate the real story (Cohn 2004).
  • Disaggregate (for Stories That Are Too Big): Break the epic into smaller, constituent stories. Each smaller piece isolates a specific slice of functionality, reducing the cognitive load and making estimation tractable (Cohn 2004).

Examples of Stories Violating the Estimable Criterion

Example 1: The Unknown Domain

As a patient, I want to receive a personalized wellness screening so that I can understand my health risks.”

  • Given I am a new patient registering on the platform, When I complete the wellness screening, Then I receive a personalized health risk summary based on my answers.
  • Independent: Yes. The screening feature does not depend on other stories.
  • Negotiable: Yes. The specific questions and screening logic are open to discussion.
  • Valuable: Yes. Personalized health screening is clearly valuable to patients.
  • Small: Yes. A single screening workflow can fit within a sprint—once the scope is clarified.
  • Testable: Yes. Acceptance criteria can define specific screening outcomes for specific patient profiles.
  • Why it violates Estimable: The developers do not know what “personalized wellness screening” means in this context. It could be a simple 5-question web form or a complex algorithm that integrates with lab data. Without domain knowledge, the team cannot estimate the effort (Cohn 2004).
  • How to fix it: Have the developers sit down with the customer (e.g., a qualified nurse or medical expert) to clarify the scope. Once the team learns it is a simple web questionnaire, they can estimate it confidently.

Example 2: The Unknown Technology

As an enterprise customer, I want to access the system’s data through a gRPC API so that I can integrate it with my existing microservices infrastructure.”

  • Given an enterprise client sends a gRPC request for user data, When the system processes the request, Then the system returns the requested data in the correct Protobuf-defined format.
  • Independent: Yes. Adding an integration interface does not depend on other stories.
  • Negotiable: Partially. The customer has specified gRPC, which is normally a technology choice that would violate Negotiable. However, in this case the customer’s existing microservices infrastructure genuinely requires gRPC compatibility, making it a hard constraint rather than an arbitrary design decision. The service contract and data schema remain open to discussion.

Note: Not all technology specifications violate Negotiable. When the customer’s existing infrastructure genuinely requires a specific protocol or format, that constraint is a hard requirement, not an arbitrary design choice. The key question is: could the user’s goal be met equally well with a different technology? If a gRPC customer cannot use REST, then gRPC is a requirement, not a design decision (Cohn 2004).

  • Valuable: Yes. Enterprise integration is clearly valuable to the purchasing organization.
  • Small: Yes. A single service endpoint can fit within a sprint—once the team understands the technology.
  • Testable: Yes. You can verify the interface returns the correct data in the correct format.
  • Why it violates Estimable: No one on the development team has ever built a gRPC service or worked with Protocol Buffers. They understand what the customer wants but have no experience with the technology required to deliver it, making any estimate unreliable (Cohn 2004).
  • How to fix it: Split into two stories: (1) a time-boxed spike—”Investigate gRPC integration: spend at most two days building a proof-of-concept service”—and (2) the actual implementation story. After the spike, the team has enough knowledge to estimate the real work (Cohn 2004).

Quick Check: As a content creator, I want the platform to automatically generate accurate subtitles for my uploaded videos so that my content is accessible to hearing-impaired viewers.”

The development team has never worked with speech-to-text technology. Is this story estimable?

Reveal Answer No. The team lacks the technical knowledge required to estimate the effort — this is the "unknown technology" barrier. The fix: split into a time-boxed spike ("Spend two days evaluating speech-to-text APIs and building a proof-of-concept") and the actual implementation story. After the spike, the team will have enough experience to estimate the real work.

Small

A small story is a manageable chunk of work that can be completed within a single iteration—not so large it becomes an epic, not so small it loses meaningful context. A user story should be as small as it can be while still delivering value.

What it is and Why it Matters The “Small” criterion states that a user story should be appropriately sized so that it can be comfortably completed by the development team within a single iteration (Cohn 2004). Stories typically represent at most a few person-weeks of work; some teams restrict them to a few person-days (Wake 2003). If a story is too large, it is called an epic and must be broken down. If a story is too small, it should be combined with related stories.

This criterion matters for several fundamental reasons:

  • Predictability: Large stories are notoriously difficult to estimate accurately. The smaller the story, the higher the confidence the team has in their estimate of the effort required (Cohn 2004).
  • Risk Reduction: If a massive story spans an entire sprint (or spills over into multiple sprints), the team risks delivering zero value if they hit a roadblock. Smaller stories ensure a steady, continuous flow of delivered value.
  • Faster Feedback: Smaller stories reach a “Done” state faster, meaning they can be tested, reviewed by the product owner, and put in front of users much sooner to gather valuable feedback.

How to Evaluate It To determine if a user story is appropriately sized, ask:

  1. Is it a compound story? Words like and, or, and but in the story description (e.g., “I want to register and manage my profile and upload photos”) often indicate that multiple stories are hiding inside one. A compound story is an “epic” that aggregates multiple easily identifiable shorter stories (Cohn 2004).
  2. Can it be split while still being valuable? If a user story can be split into separate stories that are still valuable then this is often a good idea. If the smaller parts do not individually satisfy valuable, we still consider the larger user story “small”.
  3. Is it a complex, uncertain story? If the story is large because of inherent uncertainty (new technology, novel algorithm), it is a complex story and should be split into a spike and an implementation story (Cohn 2004).

How to Improve It The approach to fixing a story that violates the Small criterion depends on whether it is too big or too small:

Stories that are too big:

  • Split by Workflow Steps (CRUD): Instead of “As a job seeker, I want to manage my resume”, split along operations: create, edit, delete, and manage multiple resumes (Cohn 2004).
  • Split by Data Boundaries: Instead of splitting by operation, split by the data involved: “add/edit education”, “add/edit job history”, “add/edit salary” (Cohn 2004).
  • Slice the Cake (Vertical Slicing): Never split along technical boundaries (one story for UI, one for database). Instead, split into thin end-to-end “vertical slices” where each story touches every architectural layer and delivers complete, albeit narrow, functionality (Cohn 2004).
  • Split by Happy/Sad Paths: Build the “happy path” (successful transaction) as one story, and handle the error states (declined cards, expired sessions) in subsequent stories.

Examples of Stories Violating the Small Criterion

Example 1: The Epic (Too Big)

As a traveler, I want to plan a vacation so that I can book all the arrangements I need in one place.”

  • Given I have selected travel dates and a destination, When I search for vacation packages, Then I see available flights, hotels, and rental cars with pricing.
  • Given I have selected a flight, hotel, and rental car, When I click “Book”, Then all reservations are confirmed and I receive a booking confirmation email.
  • Independent: Yes. Planning a vacation does not overlap with other stories.
  • Negotiable: Yes. The specific features and UI are open to discussion.
  • Valuable: Yes. End-to-end vacation planning is clearly valuable to travelers.
  • Estimable: Partially. A developer can give a rough order-of-magnitude estimate (“several months”), but the hidden complexity within this epic makes the estimate too unreliable for sprint planning. Violations of Small often cause violations of Estimable, since epics contain hidden complexity (Cohn 2004).
  • Testable: Yes. Acceptance criteria can be written, though they would need to be much more detailed once the epic is broken into smaller stories.
  • Why it violates Small: “Planning a vacation” involves searching for flights, comparing hotels, booking rental cars, managing an itinerary, handling payments, and much more. This is an epic containing many stories. It cannot be completed in a single sprint (Cohn 2004).
  • How to fix it: Disaggregate into smaller vertical slices: “As a traveler, I want to search for flights by date and destination so that I can find available options”, “As a traveler, I want to compare hotel prices for my destination so that I can choose one within my budget”, etc.

Example 2: The Micro-Story (Too Small)

As a job seeker, I want to edit the date for each community service entry on my resume so that I can correct mistakes.”

  • Given I am viewing a community service entry on my resume, When I change the date field and click “Save”, Then the updated date is displayed on my resume.
  • Independent: Yes. Editing a single date field does not depend on other stories.
  • Negotiable: Yes. The exact editing interaction is open to discussion.
  • Valuable: Yes. Correcting resume data is valuable to the user.
  • Estimable: Yes. Editing a single field is trivially estimable.
  • Testable: Yes. Clear pass/fail criteria can be written.
  • Why it violates Small: This story is too small. The administrative overhead of writing, estimating, and tracking this story card takes longer than actually implementing the change. Having dozens of stories at this granularity buries the team in disconnected details—what Wake calls a “bag of leaves” (Wake 2003).
  • How to fix it: Combine with related micro-stories into a single meaningful story: “As a job seeker, I want to edit all fields of my community service entries so that I can keep my resume accurate.” (Cohn 2004)

Quick Check: As a job seeker, I want to manage my resume so that employers can find me.”

Is this story appropriately sized?

Reveal Answer No — it is too big (an epic). "Manage my resume" hides multiple stories: create a resume, edit sections, upload a photo, delete a resume, manage multiple versions. The word "manage" is often a signal that a story is a compound epic. Split by CRUD operations: "I want to create a resume", "I want to edit my resume", "I want to delete my resume" — or by data boundaries: "I want to add/edit my education", "I want to add/edit my work history", "I want to add/edit my skills".

Testable

A testable story has clear, objective, and measurable acceptance criteria that allow the team to verify definitively when the work is done.

What it is and Why it Matters The “Testable” criterion dictates that a user story must have clear, objective, and measurable conditions that allow the team to verify when the work is officially complete. If a story is not testable, it can never truly be considered “Done”.

This criterion matters for several crucial reasons:

  • Shared Understanding: It forces the product owner and the development team to align on the exact expectations. It removes ambiguity and prevents the dreaded “that’s not what I meant” conversation at the end of a sprint.
  • Proving Value: A user story represents a slice of business value. If you cannot test the story, you cannot prove that it successfully delivers that value to the user.
  • Enabling Quality Assurance: Testable stories allow QA engineers (and developers practicing Test-Driven Development) to write their test cases—whether manual or automated—before a single line of production code is written.

How to Evaluate It To determine if a user story is testable, ask yourself the following questions:

  1. Can I write a definitive pass/fail test for this? If the answer relies on someone’s opinion or mood, it is not testable.
  2. Does the story contain “weasel words”? Look out for subjective adjectives and adverbs like fast, easy, intuitive, beautiful, modern, user-friendly, robust, or seamless. These words are red flags that the story lacks objective boundaries.
  3. Are the Acceptance Criteria clear? Does the story have defined boundaries that outline specific scenarios and edge cases?

How to Improve It If you find a story that violates the Testable criterion, you can improve it by replacing subjective language with quantifiable metrics and concrete scenarios:

  • Quantify Adjectives: Replace subjective terms with hard numbers. Change “loads fast” to “loads in under 2 seconds”. Change “supports a lot of users” to “supports 10,000 concurrent users”.
  • Use the Given/When/Then Format: Borrow from Behavior-Driven Development (BDD) to write clear acceptance criteria. Establish the starting state (Given), the action taken (When), and the expected, observable outcome (Then).
  • Define “Intuitive” or “Easy”: If the goal is a “user-friendly” interface, make it testable by tying it to a metric, such as: “A new user can complete the checkout process in fewer than 3 clicks without relying on a help menu.”

Examples of Stories Violating the Testable Criterion

Below are two user stories that are not testable but still satisfy (most) other INVEST criteria.

Example 1: The Subjective UI Requirement

As a marketing manager, I want the new campaign landing page to feature a gorgeous and modern design, so that it appeals to our younger demographic.”

  • Given the landing page is deployed, When a visitor from the 18-24 demographic views it, Then the design looks gorgeous and modern.
  • Independent: Yes. It doesn’t inherently rely on other features being built first.
  • Negotiable: Yes. The exact layout and tech used to build it are open to discussion.
  • Valuable: Yes. A landing page to attract a younger demographic provides clear business value.
  • Estimable: Yes. Generally, a frontend developer can estimate the effort to build a standard landing page independent of what specific definition of “gorgeous and modern” is used.
  • Small: Yes. Building a single landing page easily fits within a single sprint.
  • Why it violates Testable: “Gorgeous”, “modern”, and “appeals to” are completely subjective. What one developer thinks is modern, the marketing manager might think is ugly.
  • How to fix it: Tie it to a specific, measurable design system or user-testing metric. (e.g., “Acceptance Criteria: The design strictly adheres to the new V2 Brand Guidelines and passes a 5-second usability test with a 4/5 rating from a focus group of 18-24 year olds.”)

Example 2: The Vague Performance Requirement

As a data analyst, I want the monthly sales report to generate instantly, so that my workflow isn’t interrupted by loading screens.”

  • Given the database contains 5 years of sales data, When the analyst requests the monthly sales report, Then the report generates instantly.
  • Independent: Yes. Optimizing or building this report can be done independently.
  • Negotiable: Yes. The team can negotiate how to achieve the speed (e.g., caching, database indexing, background processing).
  • Valuable: Yes. Saving the analyst’s time is a clear operational benefit.
  • Estimable: Yes. A developer can estimate the effort for standard report optimizations (query tuning, caching, indexing, pagination) regardless of the specific latency threshold that will ultimately be defined. The implementation work is predictable even though the acceptance threshold is not—just as in Example 1 above, where the effort to build a landing page does not depend on the specific definition of “modern”.
  • Small: Yes. It is a focused optimization on a single report.
  • Why it violates Testable: “Instantly” is subjective. Does it mean 100 milliseconds? Two seconds? Zero perceived delay? Without a quantifiable threshold, QA cannot write a definitive pass/fail test—and the developer cannot know when to stop optimizing.
  • How to fix it: Replace the subjective word with a quantifiable service level indicator. (e.g., “Acceptance Criteria: Given the database contains 5 years of sales data, when the analyst requests the monthly sales report, then the data renders on screen in under 2.5 seconds at the 95th percentile.”)

Example 3: The Subjective Audio Requirement

As a podcast listener, I want the app’s default intro chime to play at a pleasant volume, so that it doesn’t startle me when I open the app.”

  • Given I open the app for the first time, When the intro chime plays, Then the volume is at a pleasant level.
  • Independent: Yes. Adjusting the audio volume doesn’t rely on other features.
  • Negotiable: Yes. The exact decibel level or method of adjustment is open to discussion.
  • Valuable: Yes. Improving user comfort directly enhances the user experience.
  • Estimable: Yes. Changing a default audio volume variable or asset is a trivial, highly predictable task (e.g., a 1-point story). The developers know exactly how much effort is involved.
  • Small: Yes. It will take a few minutes to implement.
  • Why it violates Testable: “Pleasant volume” is entirely subjective. A volume that is pleasant in a quiet library will be inaudible on a noisy subway. Because there is no objective baseline, QA cannot definitively pass or fail the test.
  • How to fix it: “Acceptance Criteria: The default intro chime must be normalized to -16 LUFS (Loudness Units relative to Full Scale).”

How INVEST supports agile processes like Scrum

The INVEST principles matter because they act as a compass for creating high-quality, actionable user stories that align with Agile goals and principles of processes like Scrum. By ensuring stories are Independent and Small, teams gain the scheduling flexibility needed to implement and release features in any order within short iterations. If user stories are not independent, it becomes hard to always select the highest value user stories. If they are not small, it becomes hard to select a Sprint Backlog that fits the team’s velocity.
Negotiable stories promote essential dialog between developers and stakeholders, while Valuable ones ensure that every effort translates into a meaningful benefit for the user. Finally, stories that are Estimable and Testable provide the clarity required for accurate sprint planning and objective verification of the finished product. In Scrum and XP, user stories are estimated during the Planning activity.

FAQ on INVEST

How are Estimable and Testable different?

Estimable refers to the ability of developers to predict the size, cost, or time required to deliver a story. This attribute relies on the story being understood well enough and having a clear enough scope to put useful bounds on those guesses.

Testable means that a story can be verified through objective acceptance criteria. A story is considered testable if there is a definitive “Yes” or “No” answer to whether its objectives have been achieved.

In practice, these two are closely linked: if a story is not testable because it uses vague terms like “fast” or “high accuracy”, it becomes nearly impossible to estimate the actual effort needed to satisfy it. But that is not always the case.

Here are examples of user stories that isolate those specific violations of the INVEST criteria:

Violates Testable but not Estimable User Story: As a site administrator, I want the dashboard to feel snappy when I log in so that I don’t get frustrated with the interface.”

  • Why it violates Testable: Terms like “snappy” or “fast” are subjective. Without a specific metric (e.g., “loads in under 2 seconds”), there is no objective “Yes” or “No” answer to determine if the story is done.
  • Why it is still Estimable: The developers know the dashboard and its tech stack well. Regardless of how “snappy” is ultimately defined, they can estimate the effort for standard front-end optimizations (lazy loading, caching, query tuning) that would improve perceived responsiveness. The implementation work is predictable even though the acceptance threshold is not, because for all reasonable interpretations of snappy, the implementation effort is roughly the same, as these techniques are well understood and often available in libraries. Note: Depending on your personal experience with web development, you might evaluate this example as not estimable. That would also be a valid judgment. In that case, check out the Subjective UI Requirement Example above for another example.

Violates Estimable but not Testable User Story: As a safety officer, I want the system to automatically identify every pedestrian in this complex, low-light video feed so that I can monitor crosswalk safety without reviewing hours of footage manually.”

  • Why it violates Estimable: This is a “research project”. Because the technical implementation is unknown or highly innovative, developers cannot put useful bounds on the time or cost required to solve it.
  • Why it is still Testable: It is perfectly testable; you could poll 1,000 humans to verify if the software’s identifications match reality. The outcome is clear, but the effort to reach it is not.
  • What about Small? This user story also violates Small—it is a very large feature that would span multiple sprints. However, the key insight is that even if we broke it into smaller pieces, each piece would still be unestimable due to the technical uncertainty. The Estimable violation is the root cause here, not the size.

How are Estimable and Small different?

While they are related, Estimable and Small focus on different dimensions of a user story’s readiness for development.

Estimable: Predictability of Effort

Estimable refers to the developers’ ability to provide a reasonable judgment regarding the size, cost, or time required to deliver a story.

  • Requirements: For a story to be estimable, it must be understood well enough and be stable enough that developers can put “useful bounds” on their guesses.
  • Barriers: A story may fail this criterion if developers lack domain knowledge, technical knowledge (requiring a “technical spike” to learn), or if the story is so large (an epic) that its complexity is hidden.
  • Goal: It ensures the Product Owner can prioritize stories by weighing their value against their cost.

Small: Manageability of Scope

Small refers to the physical magnitude of the work. A story should be a manageable chunk that can be completed within a single iteration or sprint.

  • Ideal Size: Most teams prefer stories that represent between half a day and two weeks of work.
  • Splitting: If a story is too big, it should be split into smaller, still-valuable “vertical slices” of functionality. However, a story shouldn’t be so small (like a “bag of leaves”) that it loses its meaningful context or value to the user.
  • Goal: Smaller stories provide more scheduling flexibility and help maintain momentum through continuous delivery.

Key Differences

  1. Nature of the Constraint: Small is a constraint on volume, while Estimable is a constraint on clarity.
  2. Accuracy vs. Size: While smaller stories tend to get more accurate estimates, a story can be small but still unestimable. For example, a “Research Project” or investigative spike might involve a very small amount of work (reading one document), but because the outcome is unknown, it remains impossible to estimate the time required to actually solve the problem.
  3. Predictability vs. Flow: Estimability is necessary for planning (knowing what fits in a release), while Smallness is necessary for flow (ensuring work moves through the system without bottlenecks).

Is there often a tradeoff between Small and Valuable?

Yes! When writing user stories this is one of the most common trade-offs to consider. The more valuable a user story is, the larger it becomes. When considering this trade-off the best advice would be to think of valuable as a binary dimension. Once a user story adds some reasonable value to the user, we consider it valuable. So aiming to write the smallest user stories that are still valuable is often a good approach. Optimizing for small until the user story becomes not valuable anymore. A user story can become too small when writing and estimating it takes more time than implementing it. Then it should be combined with other user stories even if the smaller user story is still somewhat valuable. Whether a user story is “good” or “bad” is not a binary criterion, but a spectrum. Aiming to reasonably improve user stories is a desirable goal, but in a practical setting, “good enough” is often sufficient while “perfect” can be a waste of time.

Is INVEST evaluated primarily on the main body of the user story or the acceptance criteria?

Since acceptance critiera define the actual scope of what defines a correct implementation of the requirement, they are the decision driver for INVEST. The main body can be seen as a gentle summary. But for INVEST the acceptance criteria usually “overrule” the main body of the user story.

Common mistakes in user stories

Acceptance criteria omit an essential step, yet the story is claimed to be “Valuable” E.g., a user story about blocking a user whose acceptance criteria include “given I have blocked a user” but never specify how the user actually performs the block.

Dependent stories are claimed to be “Independent” E.g., a story for creating a post and a story for liking a post are marked independent, even though liking requires a post to exist. E.g., a story for logging in and a story for creating or liking a post are marked independent, even though the latter presupposes authentication.

”So that…” is circular or merely restates the feature E.g., “As a user, I want to like/unlike a post on my feed so that I can engage and interact with the content.” Engage is just a synonym for like/unlike, and content is just a synonym for post — the rationale explains nothing. A good “so that” states the underlying motivation: e.g., “so that I can signal approval to the author.”

Acceptance criteria are missing the key assertion E.g., “Given I am on the login screen, when I enter the correct email and password and click Login, then I should be redirected to the home screen.” Being redirected to the home screen does not confirm a successful login. The criterion should also assert that the user is authenticated — for example, that their name appears in the header or that they can access protected content.

Applicability

User stories are ideal for iterative, customer-centric projects where requirements might change frequently.

Limitations

User stories can struggle to capture non-functional requirements like performance, security, or reliability, and they are generally considered insufficient for safety-critical systems like spacecraft or medical devices.

Practice

User Stories & INVEST Principle Flashcards

Test your knowledge on Agile user stories and the criteria for creating high-quality requirements!

Difficulty: Intermediate

What is the primary purpose of Acceptance Criteria in a user story?

Difficulty: Basic

What is the standard template for writing a User Story?

Difficulty: Basic

What does the acronym INVEST stand for?

Difficulty: Basic

What does ‘Independent’ mean in the INVEST principle?

Difficulty: Intermediate

Why must a user story be ‘Negotiable’?

Difficulty: Intermediate

What makes a user story ‘Estimable’?

Difficulty: Basic

Why is it crucial for a user story to be ‘Small’?

Difficulty: Basic

How do you ensure a user story is ‘Testable’?

Difficulty: Basic

What is the widely used format for writing Acceptance Criteria?

Difficulty: Intermediate

What is the difference between the main body of the User Story and Acceptance Criteria?

INVEST Criteria Violations Quiz

Test your ability to identify which of the INVEST principles are being violated in various Agile user stories, now including their associated Acceptance Criteria.

Difficulty: Basic

Read the following user story and its acceptance criteria: “As a customer, I want to pay for my items using a credit card, so that I can complete my purchase”

Acceptance Criteria:

  • Given a user has items in their cart, when they enter valid credit card details and submit, then the payment is processed and an order confirmation is shown.
  • Given a user enters an expired credit card, when they submit, then the system displays an ‘invalid card’ error message.

(Note: The user stories on User Registration and Cart Management are still not implemented and still in the backlog)
Which INVEST criteria are violated? (Select all that apply)

Correct Answers:
Difficulty: Intermediate

Read the following user story and its acceptance criteria: “As a user, I want the application to be built using a React.js frontend, a Node.js backend, and a PostgreSQL database, so that I can view my profile.”

Acceptance Criteria:

  • Given a user is logged in, when they navigate to the profile route, then the React.js components mount and display their data.
  • Given a profile update occurs, when the form is submitted, then a REST API call is made to the Node.js server to update the PostgreSQL database.

Which INVEST criteria are violated? (Select all that apply)

Correct Answers:
Difficulty: Intermediate

Read the following user story and its acceptance criteria: “As a developer, I want to add a hidden ID column to the legacy database table that is never queried, displayed on the UI, or used by any background process, so that the table structure is updated.”

Acceptance Criteria:

  • Given the database migration script runs, when the legacy table is inspected, then a new integer column named ‘hidden_id’ exists.
  • Given the application is running, when any database operation occurs, then the ‘hidden_id’ column remains completely unused and unaffected.

Which INVEST criteria are violated? (Select all that apply)

Correct Answers:
Difficulty: Intermediate

Read the following user story and its acceptance criteria: “As a hospital administrator, I want a comprehensive software system that includes patient records, payroll, pharmacy inventory management, and staff scheduling, so that I can run the entire hospital effectively.”

Acceptance Criteria:

  • Given a doctor is logged in, when they search for a patient, then their full medical history is displayed.
  • Given it is the end of the month, when HR runs payroll, then all staff are paid accurately.
  • Given the pharmacy receives a shipment, when it is logged, then the inventory updates automatically.
  • Given a nursing manager opens the calendar, when they drag and drop shifts, then the schedule is saved and notifications are sent to staff.

Which INVEST criteria are violated? (Select all that apply)

Correct Answers:
Difficulty: Basic

Read the following user story and its acceptance criteria: “As a website visitor, I want the homepage to load blazing fast and look extremely modern, so that I have a pleasant browsing experience.”

Acceptance Criteria:

  • Given a user enters the website URL, when they press enter, then the page loads blazing fast.
  • Given the homepage renders, when the user looks at the UI, then the design feels extremely modern and pleasant.

Which INVEST criteria are violated? (Select all that apply)

Correct Answers:

Acknowledgements

Thanks to Allison Gao for constructive suggestions on how to improve this chapter.

UML


Unified Modeling Language (UML)

Why Model?

Before writing a single line of code, software engineers need to communicate their ideas clearly. Consider a team of four developers asked to build “a building management system”. Without a shared model, each person imagines something different—one pictures a skyscraper, another a shopping mall, a third a house. A model gives the team a shared blueprint to align on, just like an architectural drawing does for a construction crew.

Modeling serves two critical purposes in software engineering:

1. Communication. Models provide a common, simple, graphical representation that allows developers, architects, and stakeholders to discuss the workings of the software. When everyone reads the same diagram, the team converges on the same understanding.

2. Early Problem Detection. Fixing bugs found during design costs a fraction of fixing bugs found during testing or maintenance. Studies have suggested that the cost to fix a defect grows substantially from the requirements phase to the maintenance phase — common estimates range from 10× to 100× depending on the project and phase (Boehm, Software Engineering Economics, 1981; McConnell, Code Complete, 2nd ed., 2004). The empirical strength of the 100× claim is debated (see Bossavit, The Leprechauns of Software Engineering, 2015), but the qualitative principle — earlier defects are cheaper to fix — is widely accepted. Modeling and analysis shifts the discovery of problems earlier in the lifecycle, where they are cheaper to fix.

What Is a Model?

A model describes a system at a high level of abstraction. Models are abstractions of a real-world artifact (software or otherwise) produced through an abstraction function that preserves the essential properties while discarding irrelevant detail. Models can be:

  • Descriptive: Documenting an existing system (e.g., reverse-engineering a legacy codebase).
  • Prescriptive: Specifying a system that is yet to be built (e.g., designing a new feature).

A Brief History of UML

In the 1980s, the rise of Object-Oriented Programming spawned dozens of competing modeling notations. By the mid-1990s, more than 50 OO modeling methods had been proposed. The three leading notation designers — Grady Booch (Booch method), Jim Rumbaugh (OMT — Object Modeling Technique), and Ivar Jacobson (OOSE — Object-Oriented Software Engineering) — converged at Rational Software and combined their approaches. This convergence, standardized by the Object Management Group (OMG) in 1997, produced UML 1.x (UML 1.1 was the first OMG-adopted version). UML 2.0 was adopted by the OMG in 2003 and finalized in 2005 (see Rumbaugh, Jacobson & Booch, The Unified Modeling Language Reference Manual, 2nd ed., 2004). The current version, UML 2.5.1 (2017), is maintained by the OMG.

UML is a large language — the current UML 2.5.1 specification spans nearly 800 pages — but in practice only a small fraction of its notation is widely used. Martin Fowler (UML Distilled) advocates learning the “mythical 20 percent of UML that helps you do 80 percent of your work”, and recommends sketching-level UML over exhaustive coverage of every symbol. This textbook follows that philosophy.

Modeling Guidelines

  • Purpose first. Before drawing, decide why the diagram exists: requirements gathering, analysis, design, or documentation. Each level shows different detail (Ambler, The Elements of UML 2.0 Style, G87–G88).
  • Nearly everything in UML is optional — you choose how much detail to show.
  • Models are rarely complete. They capture only the aspects relevant to the question at hand (Fowler’s “Depict Models Simply” principle).
  • UML is open to interpretation and designed to be extended via profiles and stereotypes.
  • 7±2 rule: Keep a single diagram to roughly 9 elements or fewer. If a diagram grows past that, split it — the cognitive load of reading it exceeds working memory.

UML Diagram Types

UML diagrams fall into two broad categories:

Static Modeling (Structure)

Static diagrams capture the fixed, code-level relationships in the system:

  • Class Diagrams (widely used) — Show classes, their attributes, operations, and relationships.
  • Package Diagrams — Group related classes into packages.
  • Component Diagrams (widely used) — Show high-level components and their interfaces.
  • Deployment Diagrams — Show the physical deployment of software onto hardware.

Behavioral Modeling (Dynamic)

Behavioral diagrams capture the dynamic execution of a system:

  • Use Case Diagrams (widely used) — Capture requirements from the user’s perspective.
  • Sequence Diagrams (widely used) — Show time-based message exchange between objects.
  • State Machine Diagrams (widely used) — Model an object’s lifecycle through state transitions.
  • Activity Diagrams (widely used) — Model workflows and concurrent processes.
  • Communication Diagrams — Show the same information as sequence diagrams, organized by object links rather than time.

In this textbook, we focus in depth on the five most widely used diagram types: Use Case Diagrams, Class Diagrams, Sequence Diagrams, State Machine Diagrams, and Component Diagrams.


Quick Preview

Here is a taste of each diagram type. Each is covered in detail in its own chapter.

Class Diagram

Sequence Diagram

State Machine Diagram

Use Case Diagram

UML Editor


Use Case Diagrams


UML Use Case Diagrams

Learning Objectives

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

  1. Identify the core elements of a use case diagram: actors, use cases, system boundaries, and associations.
  2. Differentiate between include, extend, and generalization relationships between use cases.
  3. Translate a written description of system requirements into a use case diagram.
  4. Evaluate when use case diagrams are appropriate versus other UML diagram types.

1. Introduction: Requirements from the User’s Perspective

Before diving into the internal design of a system (class diagrams, sequence diagrams), we need to answer a fundamental question: What should the system do? Use case diagrams capture the requirements of a system from the user’s perspective. They show the functionality a system must provide and which types of users interact with each piece of functionality.

A use case refers to a particular piece of functionality that the system must provide to a user—similar to a user story. Use cases are at a higher level of abstraction than other UML elements. While class diagrams model the code structure and sequence diagrams model object interactions, use case diagrams model the system’s goals from the outside looking in.

Concept Check (Generation): Before reading further, try to list 4-5 things a user might want to do with an online bookstore. What types of users might there be? Write your answers down, then compare them to the examples below.


2. Core Elements

2.1 Actors

An actor represents a role played by a user, or any other system, that interacts with the subject of a use case (UML 2.5.1 §18.2.1). The most common notation is a stick figure with the role name below, but the spec defines three equivalent notations: a stick figure (Figure 18.6), a class rectangle with the keyword «actor» (Figure 18.7), or a custom icon that conveys the kind of actor — for example a screen-and-keyboard icon for a non-human external system (Figure 18.8). Any of the three may be used for any actor; the choice is stylistic, not semantic.

Key points about actors:

  • An actor is a role, not a specific person. One person can play multiple roles (e.g., a university professor might be both an “Instructor” and a “Student” in a course system).
  • A single user may be represented by multiple actors if they interact with different parts of the system in different capacities.
  • Actors are always external to the subject — they interact with it but are not part of it.

⚠ Roles, not job titles (Ambler G65). Name actors for the role they play in this system, not for their position in a company. “Customer”, “Instructor”, “Support Agent” — good. “Senior VP of Sales”, “Junior CSR” — bad. Job titles change when HR reorganises; roles describe what the system cares about. The same rule applies to our auto-memory guidance: user-story actors must always be real users, never “As a system”.

Non-human actors exist. An actor can be an external system (a payment gateway, an email provider) or even Time itself — Ambler and Seidl et al. both recommend introducing a Time actor for use cases triggered on a schedule (payroll, monthly statements, nightly batch jobs). The actor convention keeps the diagram honest: something initiates every use case.

2.2 Use Cases

A use case represents a specific goal or piece of functionality the system provides. Use cases are drawn as ovals (ellipses) containing the use case name.

  • Use case names should describe a goal using a verb phrase (e.g., “Place Order”, not “Order” or “OrderSystem”).
  • There will be one or more use cases per kind of actor. It is common for any reasonable system to have many use cases.

2.3 Subject (System Boundary)

The rectangle drawn around the use cases is called the subject in the UML 2.5.1 specification — though “system boundary” is the term most textbooks and tools use, and the spec acknowledges it (§18.1.4: “A subject (sometimes called a system boundary)…”). The subject represents the system (or component, or class) that realizes the contained use cases. The subject’s name appears at the top of the rectangle. Actors are placed outside the subject, and use cases are placed inside.

2.4 Associations

An association is a line drawn from an actor to a use case, indicating that the actor participates in that use case.

Putting the Basics Together

Here is a use case diagram for an automatic train system (an unmanned people-mover like those found in airports):

Reading this diagram: A Passenger can Ride the train, and a Technician can Repair the train. Both are roles (actors) external to the system.


3. Use Case Descriptions

A use case diagram shows what functionality exists, but not how it works. To capture the details, each use case should have a written use case description that includes:

  • Name: A concise verb phrase (e.g., “Normal Train Ride”).
  • Actors: Which actors participate (e.g., Passenger).
  • Entry Condition: What must be true before this use case begins (e.g., Passenger is at station).
  • Exit Condition: What is true when the use case ends (e.g., Passenger has left the station).
  • Event Flow: A numbered list of steps describing the interaction.

Example: Normal Train Ride

Field Value
Name Normal Train Ride
Actors Passenger
Entry Condition Passenger is at station
Exit Condition Passenger has left the station

Event Flow:

  1. Passenger arrives and presses the request button.
  2. Train arrives and stops at the platform.
  3. Doors open.
  4. Passenger steps into the train.
  5. Doors close.
  6. Passenger presses the request button for their final stop.
  7. Doors open at the final stop.
  8. Passenger exits the train.

Concept Check (Self-Explanation): Look at the event flow above. What would a non-functional requirement for this system look like? (Hint: Think about timing, safety, or capacity.) Non-functional requirements are not captured in use case diagrams—they are typically captured as Quality Attribute Scenarios.


4. Relationships Between Use Cases

Use cases rarely exist in isolation. UML defines three types of relationships between use cases: inclusion, extension, and generalization. Each is drawn as a dashed or solid arrow between use cases.

Notation Rule: For include and extend arrows, the arrows are dashed with an open arrowhead (UML 2.5.1 §18.1.4) and point in the reading direction of the verb. The relationship label is written in guillemets — the spec uses «include» and «extend»; the ASCII shorthand <<include>> / <<extend>> used throughout this chapter is universally accepted by tools and equivalent. Use the base form of the verb (e.g., «include», not «includes»).

4.1 Inclusion (<<include>>)

A use case can include the behavior of another use case. This means the included behavior always occurs as part of the including use case. Think of it as mandatory sub-behavior that has been factored out because multiple use cases share it.

Reading this diagram: Whenever a customer Purchases an Item, they always Login. Whenever they Track Packages, they also always Login. The Login behavior is shared, so it is factored out into its own use case and included by both.

Key insight: The arrow points from the including use case to the included use case (from “Purchase Item” to “Login”).

4.2 Extension (<<extend>>)

A use case extension encapsulates a distinct flow of events that is not part of the normal or basic flow but may optionally extend an existing use case. Think of it as an optional, exceptional, or conditional behavior.

Extension points (optional). A base use case can declare specific named points inside its flow where extensions may plug in — the <<extend>> relationship can name which point it attaches to, and an optional {condition} note on a dashed comment line states when the extension fires. Ambler (G83) advises skipping extension points on diagrams unless the flow is genuinely ambiguous — the detail usually fits better inside the textual use case description than on the picture.

Reading this diagram: When a customer purchases an item, debug info can (optionally) be logged in some cases. The extension is not part of the normal flow.

Key insight: The arrow points from the extending use case to the base use case (from “Log Debug Info” to “Purchase Item”). This is the opposite direction from <<include>>.

4.3 Generalization

Just like class generalization, a specialized use case can replace or enhance the behavior of a generalized use case. Generalization uses a solid line with a hollow triangle arrowhead pointing to the generalized (parent) use case.

Reading this diagram: “Synchronize Wirelessly” and “Synchronize Serially” are both specialized versions of “Synchronize Data”. Either can be used wherever the general “Synchronize Data” use case is expected.

Concept Check (Retrieval Practice): Without looking at the diagrams above, answer: Which direction does the <<include>> arrow point? Which direction does the <<extend>> arrow point? What arrowhead style does generalization use?

Reveal Answer <<include>> points from the including use case to the included use case. <<extend>> points from the extending use case to the base use case. Generalization uses a solid line with a hollow triangle.

5. Include vs. Extend: A Comparison

Students often confuse <<include>> and <<extend>>. Here is a direct comparison:

Feature <<include>> <<extend>>
When it happens Always — the included behavior is mandatory Sometimes — the extending behavior is optional/conditional
Arrow direction From base (including) use case to included use case From extending use case to base (extended) use case
Analogy Like a function call that always executes Like an optional plugin or hook
Example “Purchase Item” always includes “Login” “Purchase Item” may be extended by “Apply Coupon”

6. Putting It All Together: Library System

Let’s read a complete use case diagram that combines all the elements we have learned.

System Walkthrough

  1. Actors: There is one actor, Customer, who interacts with the library system.
  2. Use Cases: The system provides three pieces of functionality: Loan Book, Borrow Book, and Check Identity.
  3. Associations: The Customer can Loan a Book or Borrow a Book.
  4. Inclusion: Both Loan Book and Borrow Book always include checking the customer’s identity. This shared behavior is factored out rather than duplicated.

Think-Pair-Share: In English, describe what this use case diagram says. What would happen if we added an <<extend>> relationship from a new use case “Charge Late Fee” to “Loan Book”?


Real-World Examples

These three examples show use case diagrams applied to modern platforms. Pay close attention to the direction of arrows and the distinction between <<include>> (always happens) and <<extend>> (sometimes happens) — this is the most commonly confused aspect of use case diagrams.


Example 1: GitHub — Repository Collaboration

Scenario: A shared codebase has three types of actors: contributors who submit code, maintainers who review and merge, and an automated CI bot. CI checks are mandatory before merging — this is an <<include>>, not an <<extend>>.

Reading the diagram:

  1. CI Bot as a non-human actor: Actors don’t have to be people. Any external role that interacts with the system qualifies — automated services, payment providers, external APIs. The CI bot initiates the Run CI Checks use case just as a human would trigger any other.
  2. <<include>> (Create PR → Authenticate): You cannot create a PR without being logged in. This is mandatory, unconditional behavior — <<include>> is correct. The arrow points from the base toward the included behavior.
  3. <<include>> (Merge PR → Run CI Checks): A maintainer cannot merge without CI passing. The checks run automatically as part of every merge — they are not optional. This is another <<include>>.
  4. What is NOT shown: There is no <<extend>> here, because there is no optional behavior in this workflow. Not every use case diagram needs <<extend>> — use it only when behavior genuinely sometimes happens.
  5. Modeling simplification: In reality every GitHub action requires authentication, so Review Code and Merge Pull Request would each <<include>> Authenticate too. We show authentication only on Create Pull Request to keep the diagram readable — don’t read this as “review and merge are unauthenticated”. Real diagrams often face the same trade-off between completeness and clarity.

Example 2: Airbnb — Accommodation Booking

Scenario: Guests search and book; hosts list properties; payment is handled by an external service. Leaving a review is optional behavior that extends the booking flow — making this an <<extend>>.

Reading the diagram:

  1. <<include>> (Booking → Payment): Every booking always processes payment. There is no booking without payment — the arrow points from Book Accommodation toward Process Payment.
  2. <<extend>> (Review → Booking): A guest may leave a review after a booking, but they don’t have to. The <<extend>> arrow points from the optional use case (Leave Review) toward the base use case (Book Accommodation) — the opposite direction from <<include>>.
  3. Payment Service as an external actor: The payment provider lives outside the Airbnb platform boundary. Showing it as an actor with an association to Process Payment makes the external dependency visible in the requirements model.
  4. Arrow direction summary: <<include>> points toward the behavior that is always included; <<extend>> points toward the base use case being sometimes extended. Both use dashed arrows — only the direction differs.

Example 3: University LMS — Canvas-Style Learning Platform

Scenario: Students submit assignments and view grades; instructors grade and post announcements. Both roles require authentication for sensitive operations. Email notifications are optional — they extend the announcement flow.

Reading the diagram:

  1. Multiple use cases sharing one <<include>> target: Both Submit Assignment and Grade Submission include Authenticate. This is the real value of <<include>> — one shared behavior, referenced from many places, maintained in one spot. If authentication changes, you update it once.
  2. <<extend>> for optional notification: Send Email Notification extends Post Announcement. Sometimes an instructor sends an email alongside the announcement, sometimes they don’t. <<extend>> captures this conditionality.
  3. Role separation: Students and Instructors have distinct, non-overlapping primary interactions. A student cannot grade; an instructor is not shown submitting assignments. The diagram communicates the access control model at a glance.
  4. Authenticate has no actor association: Authenticate is never triggered directly by an actor — it is always triggered by another use case (<<include>>). This is correct — actors initiate top-level use cases, not shared sub-behaviors.

⚠ Common Use Case Diagram Mistakes

# Mistake Fix
1 <<include>> and <<extend>> arrows pointing the wrong way Remember (UML 2.5.1 §18.1.4): <<include>> points from base (including) → included; <<extend>> points from extension → base (extended). They are opposite directions.
2 Actors named with job titles instead of roles (“VP of Sales”) Name the role (“Sales Rep”). Roles describe what the system cares about; titles change with HR.
3 Missing actor on use cases — a use case with no initiator Every top-level use case must be triggered by someone (actor, external system, or Time). If nobody triggers it, why is it in the diagram?
4 Functional decomposition via <<include>> — breaking every internal step into its own use case Use cases are user-visible goals, not functions. If your diagram contains “validate input” or “query database” as use cases, you have slipped into design.
5 Modeling the GUI — use cases like “Click Save button” or “Open menu” Use cases describe what the user wants to achieve, not how they click through the UI. “Save draft” is a use case; “click the floppy-disk icon” is not.

7. Active Recall Challenge

Grab a blank piece of paper. Without looking at this chapter, try to draw the use case diagram for the following scenario:

  1. A Student can Enroll in Course and View Grades.
  2. A Professor can Create Course and Submit Grades.
  3. Both Enroll in Course and Create Course always include Authenticate (login).
  4. View Grades can optionally be extended by Export Transcript.

After drawing, review your diagram against the rules in sections 2-4. Check: Are your arrows pointing in the correct direction? Did you use dashed lines for include/extend?


8. Interactive Practice

Test your knowledge with these retrieval practice exercises.

Knowledge Quiz

UML Use Case Diagram Practice

Test your ability to read and interpret UML Use Case Diagrams.

Difficulty: Basic

In a use case diagram, what does an actor represent?

Correct Answer:
Difficulty: Intermediate

Look at this diagram. What does the <<include>> relationship mean here?

Correct Answer:
Difficulty: Intermediate

What is the key difference between <<include>> and <<extend>>?

Correct Answer:
Difficulty: Intermediate

In this diagram, what does the <<extend>> arrow mean?

Correct Answer:
Difficulty: Basic

What does the rectangle (system boundary) represent in a use case diagram?

Correct Answer:
Difficulty: Basic

Which of the following are valid elements in a UML Use Case Diagram? (Select all that apply.)

Correct Answers:
Difficulty: Advanced

How is generalization between use cases shown?

Correct Answer:
Difficulty: Advanced

A university system requires that both ‘Enroll in Course’ and ‘Drop Course’ always verify the student’s identity first. How should ‘Verify Identity’ be related to these use cases?

Correct Answer:

Retrieval Flashcards

UML Use Case Diagram Flashcards

Quick review of UML Use Case Diagram notation and relationships.

Difficulty: Basic

What does an actor represent in a use case diagram, and how is it drawn?

Difficulty: Intermediate

What is the difference between <<include>> and <<extend>>?

Difficulty: Intermediate

Which direction does the <<include>> arrow point?

Difficulty: Intermediate

Which direction does the <<extend>> arrow point?

Difficulty: Basic

What does the system boundary (rectangle) represent in a use case diagram?

Difficulty: Advanced

How is generalization between use cases drawn?

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.

Class Diagrams


Introduction

Pedagogical Note: This chapter is designed using principles of Active Engagement (frequent retrieval practice). We will build concepts incrementally. Please complete the “Quick Checks” without looking back at the text—this introduces a “desirable difficulty” that strengthens long-term memory.

🎯 Learning Objectives

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

  1. Translate real-world object relationships into UML Class Diagrams.
  2. Differentiate between structural relationships (Association, Aggregation, Composition).
  3. Read and interpret system architecture from UML class diagrams.

Diagram – The Blueprint of Software

Imagine you are an architect designing a complex building. Before laying a single brick, you need blueprints. In software engineering, we use similar models. The Unified Modeling Language (UML) is the most common one. Among UML diagrams, Class Diagrams are the most common ones, because they are very close to the code. They describe the static structure of a system by showing the system’s classes, their attributes, operations (methods), and the relationships among objects.

The Core Building Blocks

2.1 Classes

A Class is a template for creating objects. In UML, a class is represented by a rectangle divided into three compartments:

  1. Top: The Class Name.
  2. Middle: Attributes (variables/state).
  3. Bottom: Operations (methods/behavior).

2.2 Modifiers (Visibility)

To enforce encapsulation, UML uses symbols to define who can access attributes and operations:

  • + Public: Accessible from anywhere.
  • - Private: Accessible only within the class.
  • # Protected: Accessible within the class and its subclasses.
  • ~ Package/Default: Accessible by any class in the same package.

2.3 Interfaces

An Interface represents a contract. It tells us what a class must do, but not how it does it. It is denoted by the <<interface>> stereotype. Interfaces contain method signatures and usually do not declare attributes (the UML specification allows it, but I recommend not to use it)

Quick Check 1 (Retrieval Practice) Cover the screen above. What do the symbols +, -, and # stand for? Why does an interface lack an attributes compartment?

Connecting the Dots: Relationships

Software is never just one class working in isolation. Classes interact. We represent these interactions with different types of lines and arrows.

Generalization — “Is-A” Relationships

Generalization connects a subclass to a superclass. It means the subclass inherits attributes and behaviors from the parent.

  • UML Symbol: A solid line with a hollow, closed arrow pointing to the parent.

Interface Realization

When a class agrees to implement the methods defined in an interface, it “realizes” the interface.

  • UML Symbol: A dashed line with a hollow, closed arrow pointing to the interface.

Dependency (Weakest Relationship)

A dependency indicates that one class uses another, but does not hold a permanent reference to it. For example, a class might use another class as a method parameter, local variable, or return type. Dependency is the weakest relationship in a class diagram.

  • UML Symbol: A dashed line with an open arrowhead.

In this example, Train depends on ButtonPressedEvent because it uses it as a parameter type in addStop(). However, Train does not store a permanent reference to ButtonPressedEvent—the dependency exists only for the duration of the method call.

Here is another example where a class depends on an exception it throws:

Association — “Has-A” / “Knows-A” Relationships

A basic structural relationship indicating that objects of one class are connected to objects of another (e.g., a “Teacher” knows about a “Student”). Attributes can also be represented as association lines: a line is drawn between the owning class and the target attribute’s class, providing a quick visual indication of which classes are related.

  • UML Symbol: A simple solid line.
  • You can also name associations and make them directional using an arrowhead to indicate navigability (which class holds a reference to the other).

Multiplicities

Along association lines, we use numbers to define how many objects are involved. Always show multiplicity on both ends of an association.

Notation Meaning
1 Exactly one
0..1 Zero or one (optional)
* or 0..* Zero to many
1..* One to many (at least one required)

When neither end of an association is annotated with an arrowhead or X mark, navigability is formally undefined in UML 2.5. By convention, many authors and tools render this case as bidirectional (both classes know about each other), but you should not rely on the default — make navigability explicit when it matters. In practice, the relationship is often one-way: only one class holds a reference to the other. UML uses arrowheads and X marks to show this navigability.

  • Navigable end An open arrowhead pointing to the class that can be “reached”. The left object has a reference to the right object.
  • Non-Navigable end An X on the end that cannot be navigated. This explicitly states that the class at the X end does not hold a reference to the other.

Here are the four navigability combinations, each with an example:

Unidirectional (one arrowhead): Only one class holds a reference.

Vote holds a reference to Politician, but Politician does not know about individual Vote objects.

Bidirectional (arrowheads on both ends): Both classes hold a reference to each other.

Employee knows about their Boss, and Boss knows about their Employee. Note that a plain line with no arrowheads on either end has unspecified navigability per UML 2.5 — not “bidirectional by default.” If you mean both directions are navigable, draw arrowheads on both ends (as above) to make that explicit.

Non-navigable on one end (X on one side): One class is explicitly prevented from navigating.

In the full UML notation, an X on the Voter end means that the opposite lifeline cannot navigate to it — i.e., Vote does not hold a reference back to Voter. (Voter’s navigability toward Vote is then determined by whatever is marked on the Vote end.) Note: the X mark is a formal UML 2 notation that many simplified tools do not render, and per UML 2.5, when one end carries a navigability arrow but the other end is unmarked, the unmarked end’s navigability is formally undefined, not “non-navigable” by default.

Non-navigable on both ends (X on both sides): Neither class holds a reference—the association is recorded only in the model, not in code.

An X on both ends of AccountClearTextPassword means neither class should store a reference to the other. This is a deliberate design decision (e.g., for security: an Account should never hold a reference to a ClearTextPassword).

When to use navigability: Navigability is a design-level detail. In analysis/domain models, plain associations (no arrowheads) are preferred because you haven’t decided which class holds the reference yet. Once you move into detailed design, add navigability to show which class stores the reference—this maps directly to code (a field/attribute in the class at the arrow tail).

Aggregation (“Owns-A”)

A specialized association where one class belongs to a collection, but the parts can exist independently of the whole. If a University closes down, the Professors still exist. Think of aggregation as a long-term, whole-part association.

  • UML Symbol: A solid line with an empty diamond at the “whole” end.

Composition (“Is-Made-Up-Of”)

A strict relationship where the parts cannot exist without the whole. If you destroy a House, the Rooms inside it are also destroyed. A part may belong to only one composite at a time (exclusive ownership), and the composite has sole responsibility for the lifetime of its parts.

  • UML Symbol: A solid line with a filled diamond at the “whole” end.
  • Per the UML spec, the multiplicity on the composite end must be 1 or 0..1.

A helpful way to think about the difference: In C++, aggregation is usually expressed through pointers/references (the part can exist separately), while composition is expressed by containing instances by value (the part’s lifetime is tied to the whole). In Java and Python, every object reference is effectively a pointer — the distinction between aggregation and composition is communicated through design intent (who created the part? who destroys it?) rather than through language syntax. Inner classes in Java are one indicator of composition but are not required.

⚠ Honest caveat on aggregation. Aggregation has intentionally informal semantics in the UML 2 specification. Martin Fowler (UML Distilled) observes: “Aggregation is strictly meaningless; as a result, I recommend that you ignore it in your own diagrams.” When you aren’t sure whether something is aggregation or plain association, use association — it is always safe. Reserve the hollow diamond for the cases where part-whole semantics clearly add communicative value.

Quick Check 2 (Self-Explanation) In your own words, explain the difference between the empty diamond (Aggregation) and the filled diamond (Composition). Give a real-world example of each that is not mentioned in this text.

Relationship Strength Summary

From weakest to strongest, the class relationships are:

RelationshipSymbolMeaningExample
Dependency Dashed arrow"uses" temporarilyMethod parameter, thrown exception
Association Solid line"knows about" structurallyEmployee knows about Boss
Aggregation Hollow diamond"has-a" (parts can exist alone)Library has Books
Composition Filled diamond"made up of" (parts die with whole)House is made of Rooms
Generalization Hollow triangle"is-a" (inheritance)Car is-a Vehicle
Realization Dashed hollow triangle"implements" (interface)Car implements Drivable

⚠ The Five Most Common UML Class Diagram Mistakes

Empirical studies of student diagrams (Chren et al., “Mistakes in UML Diagrams: Analysis of Student Projects in a Software Engineering Course”, ICSE SEET 2019) identify these recurring errors. Watch for them in your own work:

# Mistake Fix
1 Generalization arrow pointed the wrong way — triangle at the child instead of the parent The triangle always rests at the parent. Sanity-check with the “is-a” sentence: “A [child] is a [parent]”.
2 Multiplicity on the wrong end — e.g., * placed next to the “one” side Multiplicity answers “for one of the opposite class, how many of this class?” Place it next to the class being quantified.
3 Missing multiplicity on one end Per Ambler (G117), always show multiplicity on both ends of every relationship. An unlabeled end is ambiguous, not “just 1.”
4 Confusing aggregation and composition — using the filled diamond when parts are actually shared Composition = exclusive ownership and lifecycle dependency. If the part can exist without the whole, use aggregation (or plain association).
5 Verbose 0..* when * suffices Use the shorthand * for zero-or-more. The UML spec defines them as identical; * is more concise. Reserve 0..* only when contrasting explicitly with 1..* nearby.

Pedagogy tip: Before turning in any class diagram, run this five-item checklist over every relationship. Catching these five mistakes catches the majority of grading-level errors.

Advanced Class Notation

Abstract Classes and Operations

An abstract class is a class that cannot be instantiated directly—it serves as a base for subclasses. In UML, an abstract class is indicated by italicizing the class name or adding {abstract}.

An abstract operation is a method with no implementation, intended to be supplied by descendant classes. Abstract operations are shown by italicizing the operation name.

In this example, Shape is abstract (it cannot be created directly) and declares an abstract draw() method. Rectangle inherits from Shape and provides a concrete implementation of draw().

Static Members

Static (class-level) attributes and operations belong to the class itself rather than to individual instances. In UML, static members are shown underlined.

From Code to Diagram: Worked Examples

A key skill is translating between code and UML class diagrams. Let’s work through several examples that progressively build this skill.

Example 1: A Simple Class

public class BaseSynchronizer {
    public void synchronizationStarted() { }
}
class BaseSynchronizer {
public:
    void synchronizationStarted() { }
};
class BaseSynchronizer:
    def synchronization_started(self) -> None:
        pass
class BaseSynchronizer {
  synchronizationStarted(): void { }
}

Each public method becomes a + operation in the bottom compartment. The return type follows a colon after the method signature.

Example 2: Attributes and Associations

When a class holds a reference to another class, you can show it either as an attribute or as an association line (but be consistent throughout your diagram).

public class Student {
    Roster roster;

    public void storeRoster(Roster r) {
        roster = r;
    }
}

class Roster { }
class Roster { };

class Student {
public:
    void storeRoster(Roster& r) {
        roster = &r;
    }

private:
    Roster* roster = nullptr;
};
class Roster:
    pass


class Student:
    def __init__(self) -> None:
        self._roster: Roster | None = None

    def store_roster(self, roster: Roster) -> None:
        self._roster = roster
class Roster { }

class Student {
  private roster?: Roster;

  storeRoster(roster: Roster): void {
    this.roster = roster;
  }
}

Notice: in the Java version, the roster field has package visibility (~) because no access modifier was specified (Java default is package-private). Other languages express visibility differently, but the relationship is the same: Student holds a reference to a Roster.

Example 3: Dependency from Exception Handling

public class ChecksumValidator {
    public boolean execute() {
        try {
            this.validate();
        } catch (InvalidChecksumException e) {
            // handle error
        }
        return true;
    }
    public void validate() throws InvalidChecksumException { }
}

class InvalidChecksumException extends Exception { }
#include <exception>

class InvalidChecksumException : public std::exception { };

class ChecksumValidator {
public:
    bool execute() {
        try {
            validate();
        } catch (const InvalidChecksumException&) {
            // handle error
        }
        return true;
    }

    void validate() { }
};
class InvalidChecksumException(Exception):
    pass


class ChecksumValidator:
    def execute(self) -> bool:
        try:
            self.validate()
        except InvalidChecksumException:
            # handle error
            pass
        return True

    def validate(self) -> None:
        pass
class InvalidChecksumException extends Error { }

class ChecksumValidator {
  execute(): boolean {
    try {
      this.validate();
    } catch (error) {
      if (!(error instanceof InvalidChecksumException)) throw error;
      // handle error
    }
    return true;
  }

  validate(): void { }
}

The ChecksumValidator depends on InvalidChecksumException (it uses it in a throws clause and catch block) but does not store a permanent reference to it. This is a dependency, not an association.

Example 4: Composition from Inner Classes

public class MotherBoard {
    private class IDEBus { }

    private final IDEBus primaryIDE = new IDEBus();
    private final IDEBus secondaryIDE = new IDEBus();
}
class MotherBoard {
    class IDEBus { };

    IDEBus primaryIDE;
    IDEBus secondaryIDE;
};
class MotherBoard:
    class _IDEBus:
        pass

    def __init__(self) -> None:
        self._primary_ide = MotherBoard._IDEBus()
        self._secondary_ide = MotherBoard._IDEBus()
class IDEBus { }

class MotherBoard {
  private readonly primaryIDE = new IDEBus();
  private readonly secondaryIDE = new IDEBus();
}

The private part type plus owned fields indicate composition: the IDEBus instances are created and controlled by the MotherBoard.

Quick Check (Generation): Before looking at the answer below, try to draw the UML class diagram for this code:

import java.util.ArrayList;
import java.util.List;
public class Division {
    private List<Employee> division = new ArrayList<>();
    private Employee[] employees = new Employee[10];
}
Reveal Answer
The List<Employee> field suggests aggregation (the collection can grow dynamically, employees can exist independently). The array with a fixed size of 10 is a direct association with a specific multiplicity.

Putting It All Together: The E-Commerce System

Pedagogical Note: We are now combining isolated concepts into a complex schema. This reflects how you will encounter UML in the real world.

Let’s read the architectural blueprint for a simplified E-Commerce system.

System Walkthrough:

  1. Generalization: VIP and Guest are specific types of Customer.
  2. Association (Multiplicity): 1 Customer can have * (zero to many) Orders.
  3. Interface Realization: Order implements the Billable interface.
  4. Composition: An Order strongly contains 1..* (one or more) LineItems. If the order is deleted, the line items are deleted.
  5. Association: Each LineItem points to exactly 1 Product.

Real-World Examples

The following examples apply everything from this chapter to systems you interact with every day. Try reading each diagram yourself before the walkthrough — this is retrieval practice in action.

Example 1: Spotify — Music Streaming Domain Model

Scenario: An analysis-level domain model for a music streaming service. The goal is to capture what things are and how they relate — not implementation details like database schemas or network calls.

What the UML notation captures:

  1. Generalization (hollow triangle): FreeUser and PremiumUser both extend User, inheriting search() and createPlaylist(). Only PremiumUser adds download() — a capability unlocked by upgrading. The hollow triangle always points up toward the parent class.
  2. Composition (filled diamond, User → Playlist): A User owns their playlists. Deleting a user account deletes their playlists — the parts cannot outlive the whole. The filled diamond sits on the owner’s side.
  3. Aggregation (hollow diamond, Playlist → Track): A Playlist contains tracks, but tracks exist independently — the same track can appear in many playlists. Deleting a playlist does not remove the track from the catalog.
  4. Association with multiplicity (Track → Artist): Each track is performed by 1..* artists — at least one (solo) or more (collaboration). This multiplicity directly encodes a real business rule.

Analysis vs. design level: This diagram has no visibility modifiers (+, -). That is intentional — at the analysis level we model what things are and do, not encapsulation decisions. Visibility is a design-level concern added in a later phase.

Example 2: GitHub — Pull Request Design Model

Scenario: A design-level diagram (note the visibility modifiers) showing how GitHub’s code review system could be modeled internally. Notice how an interface creates a formal contract between components.

What the UML notation captures:

  1. Interface Realization (dashed hollow arrow): PullRequest implements Mergeable — a contract committing the class to provide canMerge() and merge(). A merge pipeline can work with any Mergeable object without knowing the concrete type.
  2. Composition (Repository → PullRequest): A PR cannot exist without its repository. Delete the repo, and all its PRs are deleted — the filled diamond on Repository’s side shows ownership.
  3. Composition (PullRequest → Review): A review only exists in the context of one PR. 1 *-- * reads: one PR can have zero or more reviews; each review belongs to exactly one PR.
  4. Dependency (dashed open arrow, PullRequest → CICheck): PullRequest uses CICheck temporarily — perhaps receiving it as a method parameter. It does not hold a permanent field reference, so this is a dependency, not an association.

Example 3: Uber Eats — Food Delivery Domain Model

Scenario: The domain model for a food delivery platform. This example is excellent for practicing multiplicity — every 0..1, 1, and * encodes a real business rule the engineering team must enforce.

What the UML notation captures:

  1. Customer "1" -- "*" Order: One customer can have zero orders (a new account) or many. The navigability arrow shows Customer holds the reference — in code, a Customer would have an orders collection field.
  2. Composition (Order → OrderItem): Order items only exist within an order. Cancelling the order destroys the items. The 1..* on OrderItem enforces that every order must have at least one item.
  3. OrderItem "*" -- "1" MenuItem: Each item references exactly one menu item. Many orders can reference the same menu item — deleting an order does not remove the menu item from the restaurant’s catalog.
  4. Driver "0..1" -- "0..1" Order: A driver handles at most one active delivery at a time; an order has at most one assigned driver. Before dispatch, both sides satisfy 0 — neither requires the other to exist yet. This captures a real business constraint in two characters.

Example 4: Netflix — Content Catalogue Model

Scenario: Netflix serves two fundamentally different types of content — movies (watched once) and TV shows (composed of seasons and episodes). This diagram shows how inheritance and composition work together to model a content catalog.

What the UML notation captures:

  1. Abstract class (abstract class Content): The italicised class name and {abstract} on play() signal that Content is never instantiated directly — you never watch a “content”, only a Movie or an Episode. Movie overrides play() with its own implementation. TVShow is also abstract (it inherits play() without overriding it) — you don’t play a show as a whole, you play one of its Episodes, which provides its own concrete play().
  2. Generalization hierarchy: Both Movie and TVShow extend Content, inheriting title and rating. A Movie adds duration directly; a TVShow delegates duration implicitly through its episodes.
  3. Nested composition (TVShow → Season → Episode): A TVShow is composed of seasons; each season is composed of episodes. Delete a show and the seasons disappear; delete a season and the episodes disappear. The chain of filled diamonds models this cascade.
  4. Association with multiplicity (Content → Genre): A movie or show belongs to 1..* genres (at least one — e.g., Action). A genre classifies * content items. This is a plain association — deleting a genre does not delete the content.

Example 5: Strategy Pattern — Pluggable Payment Processing

Scenario: A shopping cart needs to support multiple payment methods (credit card, PayPal, crypto) and let users switch between them at runtime. This is the Strategy design pattern — and a class diagram is the canonical way to document it.

What the UML notation captures:

  1. Interface as contract: PaymentStrategy defines the contract — pay() and refund(). Every concrete implementation must provide both. The interface appears at the top of the hierarchy, with implementors below.
  2. **Three realizations (.. >):** CreditCardPayment, PayPalPayment, and CryptoPayment all implement PaymentStrategy. The dashed hollow arrow points toward the interface each class promises to fulfill.
  3. Association ShoppingCart --> PaymentStrategy: The cart holds a reference to PaymentStrategy — not to any specific implementation. This navigability arrow (open head, not filled diamond) means ShoppingCart has a field of type PaymentStrategy. Crucially, it is typed to the interface, not a concrete class.
  4. The power of this design: Because ShoppingCart depends on PaymentStrategy (the interface), you can call cart.setPayment(new CryptoPayment()) at runtime and the cart works without any changes to its own code. The class diagram makes this extensibility visible — and it shows exactly where the seam between context and strategy is.

Connection to practice: This is the same pattern behind Java’s Comparator, Python’s sort(key=...), and every payment SDK you will ever integrate in your career. Class diagrams let you see the shape of the pattern independent of any language.

5. Chapter Review & Spaced Practice

To lock this information into your long-term memory, do not skip this section!

Active Recall Challenge: Grab a blank piece of paper. Without looking at this chapter, try to draw the UML Class Diagram for the following scenario:

  1. A School is composed of one or many Departments (If the school is destroyed, departments are destroyed).
  2. A Department aggregates many Teachers (Teachers can exist without the department).
  3. Teacher is a subclass of an Employee class.
  4. The Employee class has a private attribute salary and a public method getDetails().

Review your drawing against the rules in sections 2 and 3. How did you do? Identifying your own gaps in knowledge is the most powerful step in the learning process!

6. Practice

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

UML Class Diagram Flashcards

Quick review of UML Class Diagram notation and relationships.

Difficulty: Basic

What does the following symbol represent in a class diagram?

Difficulty: Intermediate

How do you denote a Static Method in UML Class Diagrams?

Difficulty: Intermediate

What is the difference between these two relationships?

Difficulty: Intermediate

What is the difference between Generalization and Realization arrows?

Difficulty: Intermediate

What do the four visibility symbols mean in UML?

Difficulty: Basic

What does the multiplicity 1..* mean on an association?

Difficulty: Basic

What relationship is represented in the diagram below, and when is it used?

Difficulty: Basic

How do you indicate an abstract class in UML?

Difficulty: Advanced

List the class relationships from weakest to strongest.

Difficulty: Intermediate

What does a navigable association () indicate?

UML Class Diagram Practice

Test your ability to read and interpret UML Class Diagrams.

Difficulty: Basic

Look at the following diagram. What is the relationship between Customer and Order?

Correct Answer:
Difficulty: Basic

Which of the following members are private in the class Engine?

Correct Answers:
Difficulty: Basic

What type of relationship is shown here between Graphic and Circle?

Correct Answer:
Difficulty: Basic

Which of the following relationships is shown here?

Correct Answer:
Difficulty: Intermediate

What type of relationship is shown between Payment and Processable?

Correct Answer:
Difficulty: Basic

What does the multiplicity 0..* on the Order side mean in this diagram?

Correct Answer:
Difficulty: Advanced

Looking at this e-commerce diagram, which statements are correct? (Select all that apply.)

Correct Answers:
Difficulty: Basic

What does the # visibility modifier mean in UML?

Correct Answer:
Difficulty: Intermediate

What type of relationship is shown here between Formatter and IOException?

Correct Answer:
Difficulty: Advanced

Given this Java code, what is the correct UML class diagram? java public class Student { Roster roster; public void storeRoster(Roster r) { roster = r; } }

Correct Answer:
Difficulty: Basic

How is an abstract class indicated in UML?

Correct Answer:
Difficulty: Advanced

Which of the following Java code patterns would result in a dependency (dashed arrow) relationship in UML, rather than an association? (Select all that apply.)

Correct Answers:
Difficulty: Intermediate

What does the arrowhead on this association mean?

Correct Answer:
Difficulty: Advanced

When should you add navigability arrowheads to associations in a class diagram?

Correct Answer:

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.

7. Interactive Tutorials

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

UML Class Diagram Tutorial (Python)


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 posting an event to a queue or invoking a callback you don’t wait for.
  • 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.

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.

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.

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.

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).


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.

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 rest of the enclosing fragment (its operand is performed instead of the remaining messages) 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);
  }
}

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;
  }
}

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:

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();
    }
  }
}

Quick 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

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.

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.

What the UML notation captures:

  1. Charge once, then branch on the response: The charge() call is issued before the alt fragment, and chargeResult is returned to OrderService. The alt then branches on the content of that response — never call payment twice. Putting the charge() inside both branches would imply a double charge attempt, which would be an architectural bug.
  2. 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.
  3. Guard conditions in [ ]: [chargeResult.approved] and [chargeResult.declined] are boolean guards — they must be mutually exclusive so exactly one branch fires.
  4. 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”.
  5. 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.

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.

What the UML notation captures:

  1. loop fragment: The matching service repeats the offer-cycle until a driver accepts (the loop guard [no driver has accepted] checks the response). loop models iteration — equivalent to a while loop. In practice this loop also has a timeout (e.g., a maximum number of attempts before cancellation), which would tighten the guard condition.
  2. Offer once per iteration, branch on the response: The diagram shows a single offerRide(request) per loop iteration — the driver’s response is either accepted or declined/timeout. The loop guard then decides whether to continue. Sending the same offer twice inside an alt would mistakenly model two separate offers for what is really one driver interaction.
  3. Flow continues after the loop: Once a driver accepts, the loop guard becomes false and execution exits, then 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.

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. The lifeline subscriber : SlackClient[*] represents the set of recipient clients (distinct from the original sender); the asynchronous arrow ->> shows the gateway pushes the message — this is server-pushed, not a return value. In a channel with 200 members, the loop body executes 200 times.
  3. ack after the loop: The original sender receives their acknowledgment (ack(messageId)) only after the broadcast completes. This is outside the loop — it is unconditional and happens once. Note that ack returns to sender, while delivery flows to subscriber — distinguishing these two lifelines is essential to model fan-out correctly.
  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.

Practice

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

UML Sequence Diagram Flashcards

Quick review of UML Sequence Diagram notation and fragments.

Difficulty: Basic

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

Difficulty: Basic

How is a return message drawn in a sequence diagram?

Difficulty: Intermediate

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

Difficulty: Basic

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

Difficulty: Basic

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

Difficulty: Basic

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

Difficulty: Intermediate

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

Difficulty: Advanced

What is the par combined fragment used for?

UML Sequence Diagram Practice

Test your ability to read and interpret UML Sequence Diagrams.

Difficulty: Basic

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

Correct Answer:
Difficulty: Basic

What does the dashed line in the diagram below represent?

Correct Answer:
Difficulty: Basic

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

Correct Answer:
Difficulty: Intermediate

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

Correct Answer:
Difficulty: Intermediate

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

Correct Answers:
Difficulty: Intermediate

What does the opt fragment in this diagram mean?

Correct Answer:
Difficulty: Basic

In UML sequence diagrams, what does time represent?

Correct Answer:
Difficulty: Basic

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

Correct Answer:
Difficulty: Basic

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

Correct Answer:
Difficulty: Intermediate

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

Correct Answer:
Difficulty: Advanced

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(); }

Correct Answer:
Difficulty: Advanced

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:

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:

UML Sequence Diagram Tutorial (Python)


State Machine Diagrams


UML State Machine Diagrams

🎯 Learning Objectives

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

  1. Identify the core components of a UML State Machine diagram (states, transitions, events, guards, and effects).
  2. Translate a behavioral description of a system into a syntactically correct ASCII state machine diagram.
  3. Evaluate when to use state machines versus other behavioral diagrams (like sequence or activity diagrams) in the software design process.

🧠 Activating Prior Knowledge

Before we dive into the formal UML syntax, let’s connect this to something you already know. Think about a standard vending machine. You can’t just press the “Dispense” button and expect a snack if you haven’t inserted money first. The machine has different conditions of being—it is either “Waiting for Money”, “Waiting for Selection”, or “Dispensing”.

In software engineering, we call these conditions States. The rules that dictate how the machine moves from one condition to another are called Transitions. If you have ever written a switch statement or a complex if-else block to manage what an application should do based on its current status, you have informally programmed a state machine.


1. Introduction: Why State Machines?

Software objects rarely react to the exact same input in the exact same way every time. Their response depends on their current context or state.

UML State Machine diagrams provide a visual, rigorous way to model this lifecycle. They are particularly useful for:

  • Embedded systems and hardware controllers.
  • UI components (e.g., a button that toggles between ‘Play’ and ‘Pause’).
  • Game entities and AI behaviors.
  • Complex business objects (e.g., an Order that moves from Pending -> Paid -> Shipped).

To manage cognitive load, we will break down the state machine into its smallest atomic parts before looking at a complete, complex system.


2. The Core Elements

2.1 States

A State represents a condition or situation during the life of an object during which it satisfies some condition, performs some activity, or waits for some event.

  • Initial State : The starting point of the machine, represented by a solid black circle.
  • Regular State : Represented by a rectangle with rounded corners.
  • Final State : The end of the machine’s lifecycle, represented by a solid black circle surrounded by a hollow circle (a bullseye).

2.2 Transitions

A Transition is a directed relationship between two states. It signifies that an object in the first state will enter the second state when a specified event occurs and specified conditions are satisfied.

Transitions are labeled using the following syntax: Event [Guard] / Effect

  • Event: The trigger that causes the transition (e.g., buttonPressed).
  • Guard: A boolean condition that must be true for the transition to occur (e.g., [powerLevel > 10]).
  • Effect: An action or behavior that executes during the transition (e.g., / turnOnLED()).

2.3 Internal Activities

States can have internal activities that execute at specific points during the state’s lifetime. These are written inside the state rectangle:

  • entry / — An action that executes every time the state is entered.
  • exit / — An action that executes every time the state is exited.
  • do / — An ongoing activity that runs while the object is in this state.

Internal activities are particularly useful for modeling embedded systems, UI components, and any object that needs to perform setup/teardown when entering or leaving a state.

Quick Check (Retrieval Practice): What is the difference between an entry/ action and an effect on a transition (the / action part of Event [Guard] / Effect)? Think about when each executes. The entry action runs every time the state is entered regardless of which transition was taken, while the transition effect runs only during that specific transition.

2.4 Composite States (Advanced)

A composite state is a state that contains a nested state machine inside it. Hierarchical (composite) states originate in Harel’s statecharts (1987) and were already present in UML 1.x; UML 2 formalized and extended their semantics to avoid the “spaghetti” of a flat state machine with dozens of transitions. When an object is in a composite state, it is simultaneously in exactly one of the nested substates.

Example: A downloadable video has a high-level Active state that contains substates Buffering, Playing, and Paused. From any substate, a stop() event exits the entire composite state.

This avoids drawing stop transitions from every leaf state separately — one transition at the composite level covers all of them. The UML 2 Reference Manual (Rumbaugh et al.) describes composite states as the primary tool for managing state-machine complexity.

2.5 Choice Pseudostate (Advanced)

A choice pseudostate (drawn as a small diamond, <>) is a branch point where the next state depends on a runtime condition evaluated inside the transition. Use it when a single event could lead to several outcomes and the decision belongs on the transition rather than in the state itself.

Compare to guards: A guard is evaluated before the transition fires; a choice pseudostate is evaluated during the transition, after some computation has happened. In most introductory models, guards are sufficient — reach for the choice pseudostate only when the branching logic is non-trivial.


3. Case Study: Modeling an Advanced Exosuit

To see how these pieces fit together, let’s model the core power and combat systems of an advanced, reactive robotic exosuit (akin to something you might see flying around in a cinematic universe).

When the suit is powered on, it enters an Idle state. If its sensors detect a threat, it shifts into Combat Mode, deploying repulsors. However, if the suit’s arc reactor drops below 5% power, it must immediately override all systems and enter Emergency Power mode to preserve life support, regardless of whether a threat is present.

Deconstructing the Model

  1. The Initial Transition: The system begins at the solid circle and transitions to Idle via the powerOn() event.
  2. Moving to Combat: To move from Idle to Combat Mode, the threatDetected event must occur. Notice the guard [sysCheckOK]; the suit will only enter combat if internal systems pass their checks. As the transition happens, the effect / deployUI() occurs.
  3. Cyclic Behavior: The system can transition back to Idle when the threatNeutralized event occurs, triggering the / retractWeapons() effect.
  4. Critical Transitions: The transition to Emergency Power is a completion transition guarded by [powerLevel < 5%] — it has no explicit event trigger and fires as soon as the guard becomes true while the source state is settled. Notice the brackets: per the UML 2.5.1 transition-label syntax Event [Guard] / Effect, the guard must always appear in square brackets so it is not misread as an event name. Once in this state, the only way out is a manualOverride(), leading to the Final State (system shutdown).

Real-World Examples

The exosuit above introduces the syntax. Now let’s see state machines applied to three modern systems. Each example highlights a different aspect of state machine design.


Example 1: Spotify — Music Player States

Scenario: A track player has distinct states that determine how it responds to the same button press. Pressing play does nothing when you are already playing — but it transitions correctly from Paused or Idle. This context-dependence is exactly what state machines model.

Reading the diagram:

  1. Buffering as a transitional state: When a track is requested, the player cannot play immediately — it must buffer first. The guard-free transition bufferReady fires automatically when enough data has loaded.
  2. Error handling via effect: If loading fails, loadError fires and the effect / showErrorMessage() executes before returning to Idle. One transition handles the rollback and the user feedback.
  3. skipTrack resets the buffer: Skipping while playing triggers / clearBuffer() as a transition effect, moving back to Buffering for the new track. Making side effects explicit in the diagram (rather than hiding them in code comments) is a key UML best practice.
  4. No final state: A music player runs indefinitely — there is no lifecycle end for this object. Omitting the final state is the correct choice here, not an oversight.

Example 2: GitHub — Pull Request Lifecycle

Scenario: A pull request moves through a well-defined set of states from creation to merge or closure. Guards prevent premature merging — merging broken code has real consequences in a real system.

Reading the diagram:

  1. Guards on the same event: Both Open → ChangesRequested and Open → Approved are triggered by reviewSubmitted. The guards [hasRejection] and [allApproved] select which transition fires. The same event can lead to different states — the guard is the deciding factor.
  2. Cyclic path (ChangesRequested → Open): After a reviewer requests changes, the author pushes new commits, sending the PR back to Open. State machines can loop — objects do not always progress linearly.
  3. Guard on merge ([ciPassed]): The PR stays Approved until CI passes. This is a business rule — it cannot be merged in a broken state. The diagram makes the constraint explicit without requiring you to read the code.
  4. Two final states: Both Merged and Closed are terminal states. Every PR ends one of these two ways. Multiple final states are valid and common in business process models.

Example 3: Food Delivery — Order Lifecycle

Scenario: Once placed, an order moves through a sequence of states from the restaurant’s kitchen to the customer’s door. Unlike the PR lifecycle, this flow is mostly linear — the diagram below shows the simplest case where the only cancellation path fires when the restaurant declines a freshly placed order. (A production system would also model customer-initiated cancellation from Confirmed and Preparing; we omit those arrows here to keep the happy path readable, but see the Self-Correction exercise below.)

Reading the diagram:

  1. Early exit with effect: Placed → Cancelled fires if the restaurant declines, triggering / refundPayment(). The effect makes the business rule explicit: every cancellation must trigger a refund.
  2. The happy path is visually obvious: Placed → Confirmed → Preparing → ReadyForPickup → InTransit → Delivered flows in a clear left-to-right, top-to-bottom reading. A new engineer on the team can understand the order lifecycle in 30 seconds.
  3. Effect on delivery (/ notifyCustomer()): The customer gets a push notification the moment the driver marks the order delivered. Transition effects tie business actions to the precise moment a state change occurs.
  4. Two terminal states: Delivered and Cancelled both lead to [*]. An order always ends — there is no indefinitely running lifecycle for a delivery order, unlike a server or a music player.

⚠ Common Mistakes in State Machines

# Mistake Fix
1 Conflating event and guard — writing powerLow as a state or as a guard instead of as an event trigger An event is something that happens externally (powerLow() was received); a guard is a condition evaluated when the event fires ([battery < 5%]). The label syntax is Event [Guard] / Effect — in that order.
2 No initial state — forgetting the solid black circle and entry transition Every state machine must have a clear starting point. Omit it and the diagram is ambiguous about how the object begins its life.
3 Dangling states — states that cannot be reached or cannot be left Trace every state: is there a path from the initial transition to it? Is there a way out (or is it a final state)? Both directions must be answered.
4 Overlapping guards — two transitions on the same event with guards that can be simultaneously true Guards on the same event must be mutually exclusive (e.g., [x > 0] and [x <= 0]). Otherwise the machine is non-deterministic.
5 Using a state machine for something that is not stateful — modeling a sequence of steps with no branching based on past events If the object reacts the same way to the same input regardless of history, it does not need a state machine — use an activity or sequence diagram instead.

🛠️ Retrieval Practice

To ensure these concepts are transferring from working memory to long-term retention, take a moment to answer these questions without looking back at the text:

  1. What is the difference between an Event and a Guard on a transition line?
  2. In our exosuit example, what would happen if threatDetected occurs, but the guard [sysCheckOK] evaluates to false? What state does the system remain in?
  3. Challenge: Sketch a simple state machine on a piece of paper for a standard turnstile (which can be either Locked or Unlocked, responding to the events insertCoin and push).

Self-Correction Check: If you struggled with question 2, revisit Section 2.2 to review how Guards act as gatekeepers for transitions.

Practice

Test your knowledge with these retrieval practice exercises.

UML State Machine Diagram Flashcards

Quick review of UML State Machine Diagram notation and transitions.

Difficulty: Basic

What is the syntax for a transition label in a state machine diagram?

Difficulty: Basic

What do the initial pseudostate and final state look like?

Difficulty: Intermediate

What happens when a transition’s guard condition evaluates to false?

Difficulty: Basic

How should states be named according to UML conventions?

Difficulty: Intermediate

When should you use a state machine diagram instead of a sequence diagram?

Difficulty: Advanced

What are the three types of internal activities a state can have?

Difficulty: Intermediate

Does a state machine always need a final state?

UML State Machine Diagram Practice

Test your ability to read and interpret UML State Machine Diagrams.

Difficulty: Basic

What does the solid black circle represent in a state machine diagram?

Correct Answer:
Difficulty: Basic

Given the transition label buttonPressed [isEnabled] / playSound(), which part is the guard condition?

Correct Answer:
Difficulty: Intermediate

In this diagram, what happens if threatDetected occurs but sysCheckOK is false?

Correct Answer:
Difficulty: Basic

Which of the following are valid components of a UML transition label? (Select all that apply.) Syntax: Event [Guard] / Effect

Correct Answers:
Difficulty: Basic

What does the symbol ◎ (a filled circle inside a hollow circle) represent?

Correct Answer:
Difficulty: Basic

Which of these is a well-named state according to UML conventions?

Correct Answer:
Difficulty: Intermediate

When should you choose a state machine diagram over a sequence diagram?

Correct Answer:
Difficulty: Basic

Look at this diagram. What is the effect that executes when transitioning from CombatMode to Idle?

Correct Answer:
Difficulty: Intermediate

How many states (not counting the initial pseudostate or final state) are in this diagram?

Correct Answer:
Difficulty: Advanced

In this diagram, which transition has both a guard condition and an effect?

Correct Answer:
Difficulty: Intermediate

Which of the following are true about the initial pseudostate () in a state machine diagram? (Select all that apply.)

Correct Answers:
Difficulty: Advanced

What is the difference between an entry/ internal activity and an effect on a transition (/ action)?

Correct Answer:
Difficulty: Intermediate

Does every state machine diagram need a final state?

Correct Answer:

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.

Component Diagrams


UML Component Diagrams

Learning Objectives

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

  1. Identify the core elements of a component diagram: components, interfaces, ports, and connectors.
  2. Differentiate between provided interfaces (lollipop) and required interfaces (socket).
  3. Model a system’s high-level architecture using component diagrams with appropriate connectors.
  4. Evaluate when to use component diagrams versus class diagrams or deployment diagrams.

1. Introduction: Zooming Out from Code

So far, we have worked at the level of individual classes (class diagrams) and object interactions (sequence diagrams). But real software systems are made up of larger building blocks—services, libraries, modules, and subsystems—that are assembled together. How do you show that your system has a web frontend that talks to an API gateway, which in turn connects to authentication and data services?

This is the role of UML Component Diagrams. They operate at a higher level of abstraction than class diagrams, showing the major deployable units of a system and how they connect through well-defined interfaces.

Diagram Type Level of Abstraction Shows
Class Diagram Low (code-level) Classes, attributes, methods, inheritance
Component Diagram High (architecture-level) Deployable modules, provided/required interfaces, assembly
Deployment Diagram Physical (infrastructure) Hardware nodes, artifacts, network topology

Quick Check (Prior Knowledge Activation): Think about a web application you have used or built. What are the major “pieces” of the system? (e.g., frontend, backend, database, authentication service). These pieces are what component diagrams model.


2. Core Elements

2.1 Components

A component is a modular, deployable, and replaceable part of a system that encapsulates its contents and exposes its functionality through well-defined interfaces. Think of it as a “black box” that does something useful.

In UML, a component is drawn as a rectangle with a small component icon (two small rectangles) in the upper-right corner. In our notation:

Examples of components in real systems:

  • A web frontend (React app, Angular app)
  • A REST API service
  • An authentication microservice
  • A database server
  • A message queue (Kafka, RabbitMQ)
  • A third-party payment gateway

2.2 Interfaces: Provided and Required

Components interact through interfaces. UML distinguishes two types:

Provided Interface (Lollipop) : An interface that the component implements and offers to other components. Drawn as a small circle (ball) connected to the component by a line. “I provide this service.”

Required Interface (Socket) : An interface that the component needs from another component to function. Drawn as a half-circle (socket/arc) connected to the component. “I need this service.”

Reading this diagram: OrderService provides the IOrderAPI interface (other components can call it) and requires the IPayment and IInventory interfaces (it depends on payment and inventory services to function).

2.3 Ports

A port is a named interaction point on a component’s boundary. Ports organize a component’s interfaces into logical groups. They are drawn as small squares on the component’s border.

  • An incoming port (receives requests), usually placed on the left edge.
  • An outgoing port (sends requests), usually placed on the right edge.

Reading this diagram: PaymentService has an incoming port processPayment (where other components send payment requests) and an outgoing port bankAPI (where it communicates with the external bank).

2.4 Connectors

Connectors are the lines between components (or between ports) that show communication pathways. The UML specification defines two kinds of connectors (ConnectorKindassembly or delegation):

  • Assembly Connector Joins a required interface (socket, §2.2) on one component to a matching provided interface (ball) on another — see §4 for the ball-and-socket “snap”. This is the canonical way to wire two components together in UML. In a simplified diagram (no ball-and-socket drawn), authors often use a plain solid arrow between components or ports as shorthand for the same idea.
  • Delegation Connector A connector inside a composite component that forwards an external port to a port on an internal sub-component (used in white-box views, not shown in this chapter).
  • Dependency A dashed arrow indicating a weaker “uses” or “depends on” relationship — not a connector in the strict UML sense, but commonly drawn on component diagrams for cross-cutting uses.
  • Plain Link An undirected association between components.

Quick Check (Retrieval Practice): Without looking back, name the two types of interfaces in component diagrams and their visual symbols. What is the difference between a provided and required interface?

Reveal Answer Provided interface (lollipop/ball): the component offers this service. Required interface (socket/half-circle): the component needs this service from another component.

3. Building a Component Diagram Step by Step

Let’s build a component diagram for an online bookstore, one piece at a time. This worked-example approach lets you see how each element is added.

Step 1: Identify the Components

An online bookstore might have: a web application, a catalog service, an order service, a payment service, and a database.

Step 2: Add Ports and Connect Components

Now we add the communication pathways. The web app sends HTTP requests to the catalog and order services. The order service calls the payment service. Both services query the database.

Reading the Complete Diagram

  1. WebApp has two outgoing ports: one for catalog requests and one for order requests.
  2. CatalogService receives HTTP requests and queries the Database.
  3. OrderService receives HTTP requests, calls PaymentService to charge the customer, and queries the Database.
  4. PaymentService receives charge requests from OrderService.
  5. Database receives SQL queries from both the CatalogService and OrderService.
  6. The labels on connectors (REST, gRPC, SQL) indicate the communication protocol.

4. Provided and Required Interfaces (Ball-and-Socket)

The ball-and-socket notation makes dependencies between components explicit. When one component’s required interface (socket) connects to another component’s provided interface (ball), this forms an assembly connector—the two pieces “snap together” like a ball fitting into a socket.

Reading this diagram: ShoppingCart requires the IPayment interface, and PaymentGateway provides it. The connector shows the dependency is satisfied—the shopping cart can use the payment gateway. If you wanted to swap in a different payment provider, you would only need to provide a component that satisfies the same IPayment interface.

This is the essence of loose coupling: components depend on interfaces, not on specific implementations.


5. Component Diagrams vs. Other Diagram Types

Students sometimes confuse when to use which diagram. Here is a comparison:

Question You Are Answering Use This Diagram
What classes exist and how are they related? Class Diagram
What are the major deployable parts and how do they connect? Component Diagram
Where do components run (which servers/containers)? Deployment Diagram
How do objects interact over time for a specific scenario? Sequence Diagram
What states does an object go through during its lifecycle? State Machine Diagram

Rule of thumb: If you can deploy it, containerize it, or replace it independently, it belongs in a component diagram. If it is an internal implementation detail (a class, a method), it belongs in a class diagram.

Note on UML 2 changes: In UML 1.x, a component was defined narrowly as a physical, replaceable part of a system — often modeled as a deployed file (DLL, JAR, EXE). UML 2 generalized the concept: a component is now a modular unit with contractually specified provided and required interfaces, and the spec covers both logical components (business or process components) and physical components (EJB, CORBA, COM+, .NET, WSDL components). The physical files that implement a component are now modeled separately as artifacts and shown on deployment diagrams. Older textbooks and diagrams you encounter in the wild may still mix component and artifact — be aware of the distinction when reading legacy UML.

⚠ Common Component Diagram Mistakes

# Mistake Fix
1 Drawing internal classes as components — putting every class in a rectangle with the component icon Components are architectural modules (services, libraries, subsystems). Classes belong in class diagrams. A rule of thumb: if you’d never deploy it separately, it’s not a component.
2 Confusing lollipop and socket — putting the ball on the consumer and the socket on the provider Ball (lollipop) = provided (“I offer this”). Socket (half-circle) = required (“I need this”). The ball fits into the socket.
3 Omitting protocol labels on connectors Labels like HTTPS, gRPC, SQL turn a generic “arrow” into a concrete architectural statement — a reviewer can spot sync-vs-async and firewall concerns at a glance.
4 Mixing deployment nodes with components Components live on nodes; they are not the same thing. Use a deployment diagram when you want to show where things run.
5 Too many components on one diagram Apply the 7±2 rule of working memory (Miller, 1956 — discussed in Fowler’s UML Distilled as a diagram-readability heuristic). If you need more than ~9 components, split into multiple diagrams by subsystem. Architecture diagrams are for overview — not exhaustive cataloguing.

6. Dependencies Between Components

Like class diagrams, component diagrams can show dependency relationships using dashed arrows. A dependency means one component uses another but does not have a strong structural coupling.

Here, OrderService depends on Logger and MetricsCollector for cross-cutting concerns, but these are not core architectural connections—they are auxiliary dependencies.


Real-World Examples

These three examples show component diagrams for well-known architectures. Notice how each diagram abstracts away class-level details entirely and focuses on deployable modules and their interfaces.


Example 1: Netflix — Streaming Service Architecture

Scenario: When you open Netflix and press play, your browser hits an API gateway that routes requests to three specialized backend services. This diagram shows the high-level communication structure of that system.

Reading the diagram:

  1. Ports organize communication surfaces: APIGateway has one incoming port (https) and three outgoing ports (auth, content, recs). The ports make explicit that the gateway routes — one input, three outputs.
  2. APIGateway as a hub: All external traffic enters through a single point. The gateway authenticates the request, then routes to the right backend service. The component diagram makes this routing topology visible at a glance — no code reading required.
  3. Protocol labels (HTTPS, gRPC): Labels communicate the type of coupling. The browser uses HTTPS (human-readable, firewall-friendly); internal service-to-service calls use gRPC (binary, low-latency). Different protocols communicate different architectural decisions.
  4. What is deliberately NOT shown: How ContentService stores video, how AuthService checks tokens, what database RecommendationEngine uses. Component diagrams show the seams between modules, not the internals. This is the right level of abstraction for architectural communication.

Example 2: E-Commerce — Microservices Backend

Scenario: A mobile app communicates through an API gateway to the OrderService. The OrderService depends on an internal PaymentService through a formal IPayment interface — enabling the payment provider to be swapped without touching OrderService.

Reading the diagram:

  1. Provided interface (ball, IPayment): PaymentService declares that it provides the IPayment interface. The implementation — Stripe, PayPal, or an in-house processor — is hidden behind the interface.
  2. Required interface (socket, IPayment): OrderService declares it requires IPayment. The os_req --> ps_prov connector is the assembly connector — the socket snaps into the ball, satisfying the dependency.
  3. Substitutability: Because OrderService depends on an interface, you could swap PaymentService for a MockPaymentService in tests, or switch from Stripe to PayPal in production, without changing a single line in OrderService. The diagram makes this architectural quality visible.
  4. OrderDB is a component: Databases are deployable units and belong in component diagrams. The SQL label distinguishes this connection from REST/gRPC connections at a glance.

Example 3: CI/CD Pipeline — GitHub Actions Architecture

Scenario: A developer pushes code; GitHub triggers a build; the build pushes an artifact and optionally deploys it. Slack notifications are a cross-cutting concern — modeled with a dependency (dashed arrow), not a port-based connector.

Reading the diagram:

  1. Primary connectors (solid arrows): The core data flow — GitHub triggers builds, builds push artifacts, builds trigger deployments. These are the main communication pathways of the pipeline.
  2. Dependency (dashed arrow, BuildService ..> SlackNotifier): Slack is a cross-cutting concern — the build reports status, but Slack is not part of the core build pipeline. A dashed arrow signals “I use this, but it is not a primary architectural interface.” If Slack is down, the pipeline still builds and deploys.
  3. Ports vs. no ports: SlackNotifier has a portin, but BuildService reaches it via a dependency arrow without a named port. This is intentional — the Slack integration is loose, not a structured interface contract. The diagram communicates that informality.
  4. The whole pipeline in 30 seconds: Push → build → artifact + deploy → notify. A new engineer can read the complete CI/CD flow from this diagram without opening a YAML config file. That is the core value proposition of component diagrams.

7. Active Recall Challenge

Grab a blank piece of paper. Without looking at this chapter, try to draw a component diagram for the following system:

  1. A MobileApp sends requests to an APIServer.
  2. The APIServer connects to a UserService and a NotificationService.
  3. The UserService queries a UserDatabase.
  4. The NotificationService depends on an external EmailProvider.

After drawing, review your diagram:

  • Did you use the component notation (rectangles with the component icon)?
  • Did you show ports or interfaces where appropriate?
  • Did you label your connectors with communication protocols?
  • Did you use a dashed arrow for the dependency on the external EmailProvider?

8. Practice

Test your knowledge with these retrieval practice exercises.

UML Component Diagram Flashcards

Quick review of UML Component Diagram notation and architecture-level modeling.

Difficulty: Basic

What does a component represent in a UML component diagram?

Difficulty: Basic

What is the difference between a provided interface (lollipop) and a required interface (socket)?

Difficulty: Basic

What is a port in a component diagram?

Difficulty: Intermediate

What is an assembly connector (ball-and-socket)?

Difficulty: Basic

When should you use a component diagram instead of a class diagram?

Difficulty: Basic

How is a dependency shown between components?

UML Component Diagram Practice

Test your ability to read and interpret UML Component Diagrams.

Difficulty: Basic

What level of abstraction do component diagrams operate at, compared to class diagrams?

Correct Answer:
Difficulty: Basic

In a component diagram, what does a provided interface (lollipop/ball symbol) indicate?

Correct Answer:
Difficulty: Intermediate

What is the purpose of ports (small squares on component boundaries)?

Correct Answer:
Difficulty: Basic

When would you choose a component diagram over a class diagram?

Correct Answer:
Difficulty: Basic

What does a dashed arrow between two components represent?

Correct Answer:
Difficulty: Intermediate

Which of the following are valid elements in a UML Component Diagram? (Select all that apply.)

Correct Answers:
Difficulty: Intermediate

What does the ball-and-socket notation (assembly connector) represent?

Correct Answer:
Difficulty: Advanced

A system has a ShoppingCart component that needs payment processing, and a StripeGateway component that provides it. If you want to later swap StripeGateway for PayPalGateway, what UML concept enables this?

Correct Answer:

Pedagogical Tip: Try to answer each question from memory before revealing the answer. Effortful retrieval is exactly what builds durable mental models. Come back to these tomorrow to benefit from spacing and interleaving.

Design Patterns


Overview

In software engineering, a design pattern is a common, acceptable solution to a recurring design problem that arises within a specific context. The concept did not originate in computer science, but rather in architecture. Christopher Alexander, an architect who pioneered the idea of pattern languages, defined a pattern beautifully (A Pattern Language, 1977): “Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice”.

In software development, design patterns refer to medium-level abstractions that describe structural and behavioral aspects of software. They sit between low-level language idioms (like how to efficiently concatenate strings in Java) and large-scale architectural patterns (like Model-View-Controller or client-server patterns). Structurally, they deal with classes, objects, and the assignment of responsibilities; behaviorally, they govern method calls, message sequences, and execution semantics.

Anatomy of a Pattern

A true pattern is more than simply a good idea or a random solution; it requires a structured format to capture the problem, the context, the solution, and the consequences. While various authors use slightly different templates, the fundamental anatomy of a design pattern contains the following essential elements:

  • Pattern Name: A good name is vital as it becomes a handle we can use to describe a design problem, its solution, and its consequences in a word or two. Naming a pattern increases our design vocabulary, allowing us to design and communicate at a higher level of abstraction.
  • Context: This defines the recurring situation or environment in which the pattern applies and where the problem exists.
  • Problem: This describes the specific design issue or goal you are trying to achieve, along with the constraints symptomatic of an inflexible design.
  • Forces: This outlines the trade-offs and competing concerns that must be balanced by the solution.
  • Solution: This describes the elements that make up the design, their relationships, responsibilities, and collaborations. It specifies the spatial configuration and behavioral dynamics of the participating classes and objects.
  • Consequences: This explicitly lists the results, costs, and benefits of applying the pattern, including its impact on system flexibility, extensibility, portability, performance, and other quality attributes.

GoF Design Patterns

The GoF (Gang of Four) design patterns are organized into three categories based on the type of design problem they address:

The full GoF catalog contains 23 patterns (5 creational, 7 structural, 11 behavioral). The lists below cover the subset we treat in detail in this chapter; the remaining GoF patterns (Prototype; Bridge, Decorator, Flyweight, Proxy; Chain of Responsibility, Interpreter, Iterator, Memento, Template Method) are equally important and worth studying from the original catalog.

Creational Patterns address the problem of object creation—how to instantiate objects in a flexible, decoupled way:

  • Factory Method: Defines an interface for creating an object but lets subclasses decide which class to instantiate, deferring creation to subclasses.
  • Abstract Factory: Provides an interface for creating families of related objects without specifying their concrete classes.
  • Builder: Separates step-by-step construction of a complex object from the representation being built.
  • Singleton: Ensures a class has only one instance while providing a controlled global point of access to it.

Structural Patterns address the problem of class and object composition—how to assemble objects and classes into larger structures:

  • Adapter: Converts the interface of a class into another interface clients expect, letting classes work together that otherwise couldn’t due to incompatible interfaces.
  • Composite: Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.
  • Façade: Provides a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use.

Behavioral Patterns address the problem of object interaction and responsibility—how objects communicate and distribute work:

  • Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime, letting the algorithm vary independently from clients that use it.
  • Observer: Establishes a one-to-many dependency between objects, ensuring that dependent objects are automatically notified and updated whenever the subject’s state changes.
  • Command: Encapsulates a request as an object, allowing invokers to be configured with different actions and supporting undo, queuing, logging, and macro commands.
  • State: Encapsulates state-based behavior into distinct classes, allowing a context object to dynamically alter its behavior at runtime by delegating operations to its current state object.
  • Mediator: Encapsulates how a set of objects interact by introducing a mediator object that centralizes complex communication logic.
  • Visitor: Represents operations over a stable object structure as separate visitor objects, making new operations easier to add without changing element classes.

These categories help practitioners narrow down which pattern might apply: if the problem is about creating objects flexibly, look at creational patterns; if it is about structuring relationships between classes, look at structural patterns; if it is about coordinating behavior between objects, look at behavioral patterns.

Beyond the GoF: PLoP-era extensions

The Pattern Languages of Program Design (PLoP) series, edited by Coplien, Schmidt, and others, formalized many additional patterns that complement the GoF catalog. The most widely adopted is the Null Object pattern, written up by Bobby Woolf in PLoP3 (1998): provide a surrogate that shares the same interface as a real collaborator but does nothing meaningful. Null Object combines naturally with Strategy (Null Strategy), State (Null State), and Iterator (Null Iterator) — see Pattern Compounds below.

Code Example: Same Design Shape, Different Syntax

Design patterns are not language features. The same responsibility split can be expressed in Java, C++, Python, or TypeScript, with each language using its own idioms. This tiny action example has the same shape as a request object: a button stores something executable without knowing the concrete operation behind it.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface Action {
    void execute();
}

final class SaveAction implements Action {
    public void execute() {
        System.out.println("Saving document");
    }
}

final class Button {
    private final Action action;

    Button(Action action) {
        this.action = action;
    }

    void click() {
        action.execute();
    }
}

public class Demo {
    public static void main(String[] args) {
        new Button(new SaveAction()).click();
    }
}
#include <iostream>

struct Action {
    virtual ~Action() = default;
    virtual void execute() = 0;
};

class SaveAction : public Action {
public:
    void execute() override {
        std::cout << "Saving document\n";
    }
};

class Button {
public:
    explicit Button(Action& action) : action_(action) {}

    void click() {
        action_.execute();
    }

private:
    Action& action_;
};

int main() {
    SaveAction save;
    Button(save).click();
}
from abc import ABC, abstractmethod


class Action(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass


class SaveAction(Action):
    def execute(self) -> None:
        print("Saving document")


class Button:
    def __init__(self, action: Action) -> None:
        self._action = action

    def click(self) -> None:
        self._action.execute()


Button(SaveAction()).click()
interface Action {
  execute(): void;
}

class SaveAction implements Action {
  execute(): void {
    console.log("Saving document");
  }
}

class Button {
  constructor(private readonly action: Action) {}

  click(): void {
    this.action.execute();
  }
}

new Button(new SaveAction()).click();

Architectural Patterns

Architectural patterns operate at a higher level of abstraction than GoF design patterns. While GoF patterns deal with classes, objects, and method calls, architectural patterns constrain the gross structure of an entire system. As Taylor, Medvidović, and Dashofy frame it in Software Architecture: Foundations, Theory, and Practice (2009): architectural styles are strategic while patterns are tactical design tools—a style constrains the overall architectural decisions, while a pattern provides a concrete, parameterized solution fragment.

Here are some examples of architectural patterns that we describe in more detail:

  • Model-View-Controller (MVC): The Model-View-Controller (MVC) architectural pattern decomposes an interactive application into three distinct components: a model that encapsulates the core application data and business logic, a view that renders this information to the user, and a controller that translates user inputs into corresponding state updates.

The Benefits of a Shared Toolbox

Just as a mechanic must know their toolbox, a software engineer must know design patterns intimately—understanding their advantages, disadvantages, and knowing precisely when (and when not) to use them.

  • A Common Language for Communication: The primary challenge in multi-person software development is communication. Patterns solve this by providing a robust, shared vocabulary. If an engineer suggests using the “Observer” or “Strategy” pattern, the team instantly understands the problem, the proposed architecture, and the resulting interactions without needing a lengthy explanation.
  • Capturing Design Intent: When you encounter a design pattern in existing code, it communicates not only what the software does, but why it was designed that way.
  • Reusable Experience: Patterns are abstractions of design experience gathered by seasoned practitioners. By studying them, developers can rely on tried-and-tested methods to build flexible and maintainable systems instead of reinventing the wheel.

Challenges and Pitfalls of Design Patterns

Despite their power, design patterns are not silver bullets. Misusing them introduces severe challenges:

  • The “Hammer and Nail” Syndrome: Novice developers who just learned patterns often try to apply them to every problem they see. Software quality is not measured by the number of patterns used. Often, keeping the code simple and avoiding a pattern entirely is the best solution. As Kent Beck advises: “Do the simplest thing that could possibly work.” This echoes Gall’s Law (John Gall, Systemantics, 1975): “A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work.”
  • Over-engineering vs. Under-engineering: Under-engineering makes software too rigid for future changes. However, over-applying patterns leads to over-engineering—creating premature abstractions that make the codebase unnecessarily complex, unreadable, and a waste of development time. Developers must constantly balance simplicity (fewer classes and patterns) against changeability (greater flexibility but more abstraction).
  • Implicit Dependencies: Patterns intentionally replace static, compile-time dependencies with dynamic, runtime interactions. This flexibility comes at a cost: it becomes harder to trace the execution flow and state of the system just by reading the code.
  • Misinterpretation as Recipes: A pattern is an abstract idea, not a snippet of code from Stack Overflow. Integrating a pattern into a system is a human-intensive, manual activity that requires tailoring the solution to fit a concrete context. As Bass, Clements, and Kazman note: “Applying a pattern is not an all-or-nothing proposition. Pattern definitions given in catalogs are strict, but in practice architects may choose to violate them in small ways when there is a good design tradeoff to be had.”

Common Student Misconceptions

Research on teaching design patterns reveals specific, recurring pitfalls that learners should be aware of:

  • Learning Structure but Not Intent: A design-structure-matrix study by Cai and Wong (CSEE&T 2011) of 85 student submissions found that 74% did not faithfully implement a modular design even though their software functioned correctly. Students learned the gross structure of patterns easily, yet they made lower-level mistakes that violated the pattern’s underlying intent—introducing extra dependencies that defeated the very modularity the pattern was meant to achieve. The lesson: correct behavior is not the same as correct design. A program can produce the right output while still being poorly structured for future change.
  • Ignoring Evolution Scenarios: The true value of a design pattern is only realized as software evolves, but student assignments, once completed, seldom evolve. Without experiencing the pain of modifying tightly coupled code, it is hard to appreciate why a pattern matters. To internalize the value of patterns, try to imagine concrete future changes (e.g., “What if we need a new type of observer?” or “What if we need to swap the database?”) and evaluate whether the design would gracefully accommodate them.
  • Confusing Patterns with Antipatterns: Just as patterns represent proven solutions, antipatterns represent common poor design choices—such as Spaghetti Code, God Class, or Lava Flow—that lead to maintainability and security issues. Recognizing antipatterns requires going beyond individual instructions into reasoning about how methods and classes are architected. Students should be exposed to both: patterns teach what good structure looks like, while antipatterns teach what to avoid.
  • The “Before and After” Exercise: A powerful technique for internalizing patterns, reported by Astrachan et al. from the first UP (Using Patterns) conference, involves taking a working solution that does not use a pattern and then refactoring it to introduce the appropriate pattern. By comparing the “before” and “after” versions—particularly when extending both with a new requirement—the concrete advantages of the pattern become viscerally clear. As the adage goes: “Good design comes from experience, and experience comes from bad design.”

Context Tailoring

It is important to remember that the standard description of a pattern presents an abstract solution to an abstract problem. Integrating a pattern into a software system is a highly human-intensive, manual activity; patterns cannot simply be misinterpreted as step-by-step recipes or copied as raw code. Instead, developers must engage in context tailoring—the process of taking an abstract pattern and instantiating it into a concrete solution that perfectly fits the concrete problem and the concrete context of their application.

Because applying a pattern outside of its intended problem space can result in bad design (such as the notorious over-use of the Singleton pattern), tailoring ensures that the pattern acts as an effective tool rather than an arbitrary constraint.

The Tailoring Process: The Measuring Tape and the Scissors

Context tailoring can be understood through the metaphor of making a custom garment, which requires two primary steps: using a “measuring tape” to observe the context, and using “scissors” to make the necessary adjustments.

1. Observation of Context

Before altering a design pattern, you must thoroughly observe and measure the environment in which it will operate. This involves analyzing three main areas:

  • Project-Specific Needs: What kind of evolution is expected? What features are planned for the future, and what frameworks is the system currently relying on?
  • Desired System Properties: What are the overarching goals of the software? Must the architecture prioritize run-time performance, strict security, or long-term maintainability?
  • The Periphery: What is the complexity of the surrounding environment? Which specific classes, objects, and methods will directly interact with the pattern’s participants?

2. Making Adjustments

Once the context is mapped, developers must “cut” the pattern to fit. This requires considering the broad design space of the pattern and exploring its various alternatives and variation points. After evaluating the context-specific consequences of these potential variations, the developer implements the most suitable version. Crucially, the design decisions and the rationale behind those adjustments must be thoroughly documented. Without documentation, future developers will struggle to understand why a pattern deviates from its textbook structure.

Dimensions of Variation

Every design pattern describes a broad design space containing many distinct variations. When tailoring a pattern, developers typically modify it along four primary dimensions:

Structural Variations

These variations alter the roles and responsibility assignments defined in the abstract pattern, directly impacting how the system can evolve. For example, the Factory Method pattern can be structurally varied by removing the abstract product class entirely. Instead, a single concrete product is implemented and configured with different parameters. This variation trades the extensibility of a massive subclass hierarchy for immediate simplicity.

Behavioral Variations

Behavioral variations modify the interactions and communication flows between objects. These changes heavily impact object responsibilities, system evolution, and run-time quality attributes like performance. A classic example is the Observer pattern, which can be tailored into a “Push model” (where the subject pushes all updated data directly to the observer) or a “Pull model” (where the subject simply notifies the observer, and the observer must pull the specific data it needs).

Internal Variations

These variations involve refining the internal workings of the pattern’s participants without necessarily changing their external structural interfaces. A developer might tailor a pattern internally by choosing a specific list data structure to hold observers, adding thread-safety mechanisms, or implementing a specialized sorting algorithm to maximize performance for expected data sets.

Language-Dependent Variations

Modern programming languages offer specific constructs that can drastically simplify pattern implementations. For instance, dynamically typed languages can often omit explicit interfaces, and aspect-oriented languages can replace standard polymorphism with aspects and point-cuts. However, there is a dangerous trap here: using language features to make a pattern entirely reusable as code (e.g., using include Singleton in Ruby) eliminates the potential for context tailoring. Design patterns are fundamentally about design reuse, not exact code reuse.

The Global vs. Local Optimum Trade-off

While context tailoring is essential, it introduces a significant challenge in large-scale software projects. Perfectly tailoring a pattern to every individual sub-problem creates a “local optimum”. However, a large amount of pattern variation scattered throughout a single project can lead to severe confusion due to overloaded meaning.

If developers use the textbook Observer pattern in one module, but highly customized, structurally varied Observers in another, incoming developers might falsely assume identical behavior simply because the classes share the “Observer” naming convention. To mitigate this, large teams must rely on project conventions to establish pattern consistency. Teams must explicitly decide whether to embrace diverse, highly tailored implementations (and name them distinctly) or to enforce strict guidelines on which specific pattern variants are permitted within the codebase.

Pattern Compounds

In software design, applying individual design patterns is akin to utilizing distinct compositional techniques in photography—such as symmetry, color contrast, leading lines, and a focal object. Simply having these patterns present does not guarantee a masterpiece; their deliberate arrangement is crucial. When leading lines intentionally point toward a focal object, a more pleasing image emerges. In software architecture, this synergistic combination is known as a pattern compound—a term coined by Dirk Riehle in Composite Design Patterns (OOPSLA 1997), where the recurring superimpositions of GoF roles (Composite Builder, Composite Visitor, Singleton State) were first systematically catalogued.

A pattern compound is a reoccurring set of patterns with overlapping roles from which additional properties emerge. Notably, pattern compounds are patterns in their own right, complete with an abstract problem, an abstract context, and an abstract solution. While pattern languages provide a meta-level conceptual framework or grammar for how patterns relate to one another, pattern compounds are concrete structural and behavioral unifications.

The Anatomy of Pattern Compounds

The core characteristic of a pattern compound is that the participating domain classes take on multiple superimposed roles simultaneously. By explicitly connecting patterns, developers can leverage one pattern to solve a problem created by another, leading to a new set of emergent properties and consequences.

Solving Structural Complexity: The Composite Builder

The Composite pattern is excellent for creating unified tree structures, but initializing and assembling this abstract object structure is notoriously difficult. The Builder pattern, conversely, is designed to construct complex object structures. By combining them, the Composite’s Component plays the role of the Builder’s Product abstraction, while Leaf and Composite are the concrete pieces the builder assembles into the resulting tree.

This compound yields the emergent properties of looser coupling between the client and the composite structure and the ability to create different representations of the encapsulated composite. However, as a trade-off, dealing with a recursive data structure within a Builder introduces even more complexity than using either pattern individually.

Managing Operations: The Composite Visitor and Composite Command

Pattern compounds frequently emerge when scaling behavioral patterns to handle structural complexity:

  • Composite Visitor: If a system requires many custom operations to be defined on a Composite structure without modifying the classes themselves (and no new leaves are expected), a Visitor can be superimposed. This yields the emergent property of strict separation of concerns, keeping core structural elements distinct from use-case-specific operations.
  • Composite Command: When a system involves hierarchical actions that require a simple execution API, a Composite Command groups multiple command objects into a unified tree. This allows individual command pieces to be shared and reused, though developers must manage the consequence of execution order ambiguity.

Communicating Design Intent and Context Tailoring

Pattern compounds also naturally arise when tailoring patterns to specific contexts or when communicating highly specific design intents.

  • Null State / Null Strategy: If an object enters a “do nothing” state, combining the State pattern with the Null Object pattern perfectly communicates the design intent of empty behavior. (Note that there is no Null Decorator, as a decorator must fully implement the interface of the decorated object).
  • Singleton Null Object: Because Null Objects are typically stateless, the canonical implementation shares one instance — making Null Object and Singleton one of the most frequent compounds in real codebases.
  • Singleton State: If State objects are entirely stateless—meaning they carry behavior but no data, and do not require a reference back to their Context—they can be implemented as Singletons. This tailoring decision saves memory and eases object creation, though it permanently couples the design by removing the ability to reference the Context in the future.

The Advantages of Compounding Patterns

The primary advantage of pattern compounds is that they make software design more coherent. Instead of finding highly optimized but fragmented patchwork solutions for every individual localized problem, compounds provide overarching design ideas and unifying themes. They raise the composition of patterns to a higher semantic abstraction, enabling developers to systematically foresee how the consequences of one pattern map directly to the context of another.

Challenges and Pitfalls

Despite their power, pattern compounds introduce distinct architectural and cognitive challenges:

  • Mixed Concerns: Because pattern compounds superimpose overlapping roles, a single class might juggle three distinct concerns: its core domain functionality, its responsibility in the first pattern, and its responsibility in the second. This can severely overload a class and muddle its primary responsibility.
  • Obscured Foundations: Tightly compounding patterns can make it much harder for incoming developers to visually identify the individual, foundational patterns at play.
  • Naming Limitations: Accurately naming a class to reflect its domain purpose alongside multiple pattern roles (e.g., a “PlayerObserver”) quickly becomes unmanageable, forcing teams to rely heavily on external documentation to explain the architecture.
  • The Over-Engineering Trap: As with any design abstraction, possessing the “hammer” of a pattern compound does not make every problem a nail. Developers must constantly evaluate whether the resulting architectural complexity is truly justified by the context.

Design Patterns and Refactoring

Design patterns and refactoring are deeply connected. As Tokuda and Batory demonstrated, refactorings are behavior-preserving program transformations that can automate the evolution of a design toward a pattern. The principle is straightforward: designs should evolve on an if-needed basis. Rather than speculating upfront about which patterns might be needed, start with the simplest working solution and refactor toward a pattern when code smells indicate the need.

Common code smells that suggest specific patterns:

Code Smell Suggested Pattern Why
Large if/else or switch on object state State Replace conditional logic with polymorphic state objects
Conditional dispatch selecting between alternative algorithms Strategy Extract varying algorithms into interchangeable objects
Large conditional dispatcher routing requests or actions Command Replace branch-by-branch dispatch with a configurable map of command objects
Complex object creation with many conditionals Factory Method or Abstract Factory Separate creation logic from usage logic
Client tightly coupled to incompatible third-party API Adapter Translate the foreign interface behind a wrapper
Client must orchestrate many subsystem calls Façade Hide coordination behind a simplified interface
Many-to-many dependencies between objects Mediator Centralize interaction logic
Hardcoded notification to specific dependents Observer Decouple subject from its dependents
Repeated if (collaborator != null) ... guards before delegating to a collaborator Null Object Replace the absent collaborator with a do-nothing object so call sites stay uniform

The Rule of Three provides a useful heuristic: do not apply a pattern until you have seen the need at least three times. This prevents speculative abstraction—creating flexibility for variation points that may never actually vary.

Advanced Concepts

Patterns Within Patterns: Core Principles

When analyzing various design patterns, you will begin to notice recurring micro-architectures. Design patterns are often built upon fundamental software engineering principles:

  • Delegation over Inheritance: Subclassing can lead to rigid designs and code duplication (e.g., trying to create an inheritance tree for cars that can be electric, gas, hybrid, and also either drive or fly). Patterns like Strategy, State, and Bridge solve this by extracting varying behaviors into separate classes and delegating responsibilities to them.
  • Polymorphism over Conditions: Patterns frequently replace complex if/else or switch statements with polymorphic objects. For instance, instead of conditional logic checking the state of an algorithm, the Strategy pattern uses interchangeable objects to represent different execution paths.
  • Additional Layers of Indirection: To reduce strong coupling between interacting components, patterns like the Mediator or Façade introduce an intermediate object to handle communication. While this centralizes logic and improves changeability, it can create long traces of method calls that are harder to debug.

Domain-Specific and Application-Specific Patterns

The Gang of Four patterns are generic to object-oriented programming, but patterns exist at all levels.

  • Domain-Specific Patterns: Certain industries (like Game Development, Android Apps, or Security) have their own highly tailored patterns. Because these patterns make assumptions about a specific domain, they generally carry fewer negative consequences within their niche, but they require the team to actually possess domain expertise.
  • Application-Specific Patterns: Every distinct software project will eventually develop its own localized patterns—agreed-upon conventions and structures unique to that team. Identifying and documenting these implicit patterns is one of the most critical steps when a new developer joins an existing codebase, as it massively improves program comprehension.

Conclusion

Design patterns are the foundational building blocks of robust software architecture. However, they are not a substitute for domain expertise or critical thought. The mark of an expert engineer is not knowing how to implement every pattern, but possessing the wisdom to evaluate trade-offs, carefully observe the context, and know exactly when the simplest code is actually the smartest design.

Practice

Design Patterns Fundamentals

Core concepts, categories, and principles of design patterns in software engineering.

Difficulty: Basic

What is a design pattern?

Difficulty: Basic

What are the three GoF pattern categories?

Difficulty: Intermediate

What is context tailoring?

Difficulty: Intermediate

What is a pattern compound?

Difficulty: Basic

What is the ‘Hammer and Nail’ syndrome?

Difficulty: Basic

What is the Rule of Three?

Difficulty: Intermediate

What is the difference between architectural patterns and design patterns?

Difficulty: Intermediate

What does the ‘Before and After’ teaching technique involve?

Difficulty: Advanced

What does the ‘74% of student submissions’ finding refer to?

Difficulty: Advanced

Why do experienced engineers prefer ‘do the simplest thing that could possibly work’?

Difficulty: Intermediate

What is the relationship between code smells and design patterns?

Difficulty: Intermediate

What does ‘polymorphism over conditions’ mean?

GoF Design Pattern Details

Key concepts, design decisions, and trade-offs for each individual GoF pattern covered in the course.

Difficulty: Basic

What problem does the Observer pattern solve?

Difficulty: Intermediate

Observer: Push vs. Pull model—which has tighter coupling?

Difficulty: Intermediate

What is the lapsed listener problem in Observer?

Difficulty: Advanced

What does ‘inverted dependency flow’ mean in Observer?

Difficulty: Basic

What problem does the State pattern solve?

Difficulty: Advanced

How does State differ from Strategy?

Difficulty: Advanced

State pattern: who should define state transitions?

Difficulty: Advanced

Why is Singleton considered a ‘pattern with a weak solution’ (POSA5)?

Difficulty: Expert

Name three thread-safety approaches for Singleton in Java.

Difficulty: Basic

What problem does Factory Method solve?

Difficulty: Intermediate

Factory Method vs. Abstract Factory: when to use which?

Difficulty: Advanced

What is the ‘Rigid Interface’ drawback of Abstract Factory?

Difficulty: Basic

What problem does Adapter solve?

Difficulty: Intermediate

Adapter vs. Facade vs. Decorator: what’s the key distinction?

Difficulty: Basic

What problem does Composite solve?

Difficulty: Advanced

Composite: Transparent vs. Safe design?

Difficulty: Basic

What problem does Façade solve?

Difficulty: Advanced

Facade vs. Mediator: what’s the communication direction?

Difficulty: Intermediate

What problem does Mediator solve?

Difficulty: Intermediate

Observer vs. Mediator: what’s the core difference?

Design Patterns Quiz

Test your understanding of design patterns at the Analyze and Evaluate levels of Bloom's taxonomy. These questions go beyond pattern recognition to test design reasoning.

Difficulty: Intermediate

A colleague proposes using the Observer pattern in a module that has exactly one dependent object which will never change. What is the best assessment of this decision?

Correct Answer:
Difficulty: Advanced

A student implements the Observer pattern. Their code works correctly: when the Subject changes, the Observer updates. However, the Observer’s update() method directly accesses subject.internalData (a private field accessed via reflection) rather than using subject.getState(). What is the primary design problem?

Correct Answer:
Difficulty: Intermediate

You have a Document class whose behavior depends on its state (Draft, Review, Published, Archived). Currently, every method contains a large switch statement checking this.status. Which pattern best addresses this?

Correct Answer:
Difficulty: Intermediate

A system uses the Singleton pattern for a database connection pool. A new requirement arrives: the system must support multi-tenant deployments where each tenant has its own database. What happens to the Singleton?

Correct Answer:
Difficulty: Intermediate

You need to create objects from a family of related types (Dough, Sauce, Cheese) that must always be used together consistently (e.g., NY-style ingredients vs. Chicago-style). Which creational pattern is most appropriate?

Correct Answer:
Difficulty: Basic

An existing third-party library provides a LegacyPrinter class with methods printText(String s) and printImage(byte[] data). Your system expects a ModernPrinter interface with render(Document d). Which pattern is most appropriate?

Correct Answer:
Difficulty: Intermediate

In the Composite pattern, a Menu can contain both MenuItem objects (leaves) and other Menu objects (composites). A developer declares add(MenuComponent) and remove(MenuComponent) on the abstract MenuComponent class. What design trade-off does this represent?

Correct Answer:
Difficulty: Intermediate

A smart home system has an alarm clock, coffee maker, calendar, and sprinkler that need to coordinate: “When the alarm rings on a weekday, brew coffee and skip watering.” Where should the rule “only on weekdays” live?

Correct Answer:
Difficulty: Advanced

Which of the following are valid reasons to avoid using the Singleton pattern? (Select all that apply)

Correct Answers:
Difficulty: Advanced

MVC is described as a ‘compound pattern.’ Which three patterns does it combine?

Correct Answer:
Difficulty: Advanced

The State and Strategy patterns have identical UML class diagrams. What is the key difference between them?

Correct Answer:
Difficulty: Advanced

A developer writes a TurkeyAdapter that implements the Duck interface. The quack() method calls turkey.gobble(), and the fly() method calls turkey.fly() in a loop five times (a Duck.fly() flies a long distance, but a Turkey.fly() only goes a short burst). Which aspect of this adapter introduces the most design risk?

Correct Answer:

Strategy


Problem

Many classes differ only in how they perform a particular task. A duck simulator needs many duck types that all swim and display, but each one flies and quacks differently. A text composer needs to break paragraphs into lines, but the linebreaking algorithm should be selectable: a fast greedy pass for an interactive editor, the TeX algorithm for high-quality typesetting, or a fixed-width strategy for icon grids. A payment system needs credit card, PayPal, and bank-transfer flows that all share the same checkout pipeline.

If you push every variant into a single class with conditional logic, the class quickly becomes unmaintainable:

class Duck {
    void fly(String type) {
        if (type.equals("mallard")) {
            // flap wings
        } else if (type.equals("rubber")) {
            // do nothing
        } else if (type.equals("decoy")) {
            // do nothing
        } else if (type.equals("rocket")) {
            // launch rockets
        }
        // every new duck adds another branch
    }
}

If you push every variant into its own subclass, you end up with deep inheritance hierarchies that fight reality: a RubberDuck inherits a fly() it must override to do nothing; a DecoyDuck inherits both fly() and quack() it must neutralize. Adding a new behavior axis (e.g., “swim with rockets”) combinatorially explodes the class hierarchy.

The core problem is: How can we vary an algorithm independently of the objects that use it, swap algorithms at runtime, and add new algorithms without touching existing client code?

Context

The Strategy pattern (also known as the Policy pattern (Gamma et al. 1995)) applies when:

  • Many related classes differ only in their behavior. Strategies provide a way to configure a class with one of many behaviors, instead of creating a subclass for each behavior (Gamma et al. 1995).
  • You need different variants of an algorithm. For example, algorithms that reflect different space/time trade-offs, or algorithms tuned for different data shapes.
  • An algorithm uses data that clients shouldn’t know about. Hiding algorithm-specific data structures behind a Strategy interface keeps clients decoupled from implementation details.
  • A class defines many behaviors that appear as multiple conditional statements. Move the conditional branches into their own Strategy classes so each branch becomes a polymorphic object (Freeman and Robson 2020).

Common applications include sorting and searching algorithms, validation rules, compression formats, payment processing flows, AI agents in games, layout/linebreaking strategies in text editors, and authentication schemes.

Solution

The Strategy pattern defines a family of algorithms, encapsulates each one as an object, and makes them interchangeable at runtime. The client (the Context) holds a reference to a Strategy interface and delegates the variable behavior to it.

The pattern involves three roles:

  1. Strategy: An interface (or abstract class) declaring the operation common to all supported algorithms. The Context uses this interface to invoke the algorithm.
  2. ConcreteStrategy: A class that implements the Strategy interface with one specific algorithm.
  3. Context: The class that uses the algorithm. It holds a reference to a Strategy object and forwards work to it. The Context typically exposes a setter so the strategy can be swapped at runtime.

The key insight is composition over inheritance: instead of locking each variant into a subclass, the Context has-a Strategy and can be re-configured at any time. This is the same insight that makes the Observer and State patterns work — replace static class hierarchies with dynamic object delegation.

UML Role Diagram

Figure: the Context aggregates a Strategy and forwards work to it; ConcreteStrategies realize the interface independently. The Context never knows which concrete strategy it holds.

UML Example Diagram

The classic SimUDuck example (Freeman and Robson 2020) extracts the fly and quack behaviors out of the Duck hierarchy. Each duck has-a FlyBehavior and a QuackBehavior; the concrete strategy classes implement each variation. A MallardDuck flies with wings and quacks normally; a RubberDuck cannot fly (uses a null-object fly behavior) and squeaks instead. (The book itself names the no-op fly strategy FlyNoWay; we use FlyNullObject here to make its design role as a Null Object explicit.)

Figure: Duck delegates flying and quacking to interchangeable Strategy objects; RubberDuck swaps in FlyNullObject instead of subclassing to override.

Sequence Diagram

This sequence shows runtime reconfiguration: a ModelDuck starts with a no-op fly behavior, the client swaps in a rocket-powered strategy via setFlyBehavior, and the next performFly() call now does something completely different — without changing the Duck class.

Figure: the same Duck object exhibits two different fly behaviors across two performFly() calls — runtime swapping is the central capability Strategy enables.

Code Example

This example follows the SimUDuck design from Head First Design Patterns (Freeman and Robson 2020). The Duck class delegates to two strategy objects; concrete duck subclasses configure their strategies in the constructor; the client can swap a strategy at runtime by calling setFlyBehavior().

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface FlyBehavior {
    void fly();
}

interface QuackBehavior {
    void quack();
}

final class FlyWithWings implements FlyBehavior {
    public void fly() {
        System.out.println("Flapping wings");
    }
}

final class FlyNullObject implements FlyBehavior {
    public void fly() {
        // do nothing — can't fly
    }
}

final class FlyRocketPowered implements FlyBehavior {
    public void fly() {
        System.out.println("Flying with a rocket");
    }
}

final class Quack implements QuackBehavior {
    public void quack() {
        System.out.println("Quack!");
    }
}

abstract class Duck {
    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    void performFly() {
        flyBehavior.fly();
    }

    void performQuack() {
        quackBehavior.quack();
    }

    void setFlyBehavior(FlyBehavior fb) {
        this.flyBehavior = fb;
    }

    abstract void display();
}

final class ModelDuck extends Duck {
    ModelDuck() {
        flyBehavior = new FlyNullObject();
        quackBehavior = new Quack();
    }

    void display() {
        System.out.println("I'm a model duck");
    }
}

public class Demo {
    public static void main(String[] args) {
        Duck model = new ModelDuck();
        model.performFly();                          // does nothing
        model.setFlyBehavior(new FlyRocketPowered());
        model.performFly();                          // "Flying with a rocket"
    }
}
#include <iostream>
#include <memory>

struct FlyBehavior {
    virtual ~FlyBehavior() = default;
    virtual void fly() = 0;
};

struct QuackBehavior {
    virtual ~QuackBehavior() = default;
    virtual void quack() = 0;
};

class FlyWithWings : public FlyBehavior {
public:
    void fly() override { std::cout << "Flapping wings\n"; }
};

class FlyNullObject : public FlyBehavior {
public:
    void fly() override { /* do nothing */ }
};

class FlyRocketPowered : public FlyBehavior {
public:
    void fly() override { std::cout << "Flying with a rocket\n"; }
};

class Quack : public QuackBehavior {
public:
    void quack() override { std::cout << "Quack!\n"; }
};

class Duck {
public:
    virtual ~Duck() = default;

    void performFly() { flyBehavior_->fly(); }
    void performQuack() { quackBehavior_->quack(); }

    void setFlyBehavior(std::unique_ptr<FlyBehavior> fb) {
        flyBehavior_ = std::move(fb);
    }

    virtual void display() const = 0;

protected:
    std::unique_ptr<FlyBehavior> flyBehavior_;
    std::unique_ptr<QuackBehavior> quackBehavior_;
};

class ModelDuck : public Duck {
public:
    ModelDuck() {
        flyBehavior_ = std::make_unique<FlyNullObject>();
        quackBehavior_ = std::make_unique<Quack>();
    }

    void display() const override { std::cout << "I'm a model duck\n"; }
};

int main() {
    ModelDuck model;
    model.performFly();                                            // does nothing
    model.setFlyBehavior(std::make_unique<FlyRocketPowered>());
    model.performFly();                                            // "Flying with a rocket"
}
from abc import ABC, abstractmethod


class FlyBehavior(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass


class QuackBehavior(ABC):
    @abstractmethod
    def quack(self) -> None:
        pass


class FlyWithWings(FlyBehavior):
    def fly(self) -> None:
        print("Flapping wings")


class FlyNullObject(FlyBehavior):
    def fly(self) -> None:
        pass  # do nothing — can't fly


class FlyRocketPowered(FlyBehavior):
    def fly(self) -> None:
        print("Flying with a rocket")


class Quack(QuackBehavior):
    def quack(self) -> None:
        print("Quack!")


class Duck(ABC):
    def __init__(self) -> None:
        self.fly_behavior: FlyBehavior
        self.quack_behavior: QuackBehavior

    def perform_fly(self) -> None:
        self.fly_behavior.fly()

    def perform_quack(self) -> None:
        self.quack_behavior.quack()

    def set_fly_behavior(self, fb: FlyBehavior) -> None:
        self.fly_behavior = fb

    @abstractmethod
    def display(self) -> None:
        pass


class ModelDuck(Duck):
    def __init__(self) -> None:
        super().__init__()
        self.fly_behavior = FlyNullObject()
        self.quack_behavior = Quack()

    def display(self) -> None:
        print("I'm a model duck")


model = ModelDuck()
model.perform_fly()                            # does nothing
model.set_fly_behavior(FlyRocketPowered())
model.perform_fly()                            # "Flying with a rocket"
interface FlyBehavior {
  fly(): void;
}

interface QuackBehavior {
  quack(): void;
}

class FlyWithWings implements FlyBehavior {
  fly(): void { console.log("Flapping wings"); }
}

class FlyNullObject implements FlyBehavior {
  fly(): void { /* do nothing — can't fly */ }
}

class FlyRocketPowered implements FlyBehavior {
  fly(): void { console.log("Flying with a rocket"); }
}

class Quack implements QuackBehavior {
  quack(): void { console.log("Quack!"); }
}

abstract class Duck {
  protected flyBehavior!: FlyBehavior;
  protected quackBehavior!: QuackBehavior;

  performFly(): void {
    this.flyBehavior.fly();
  }

  performQuack(): void {
    this.quackBehavior.quack();
  }

  setFlyBehavior(fb: FlyBehavior): void {
    this.flyBehavior = fb;
  }

  abstract display(): void;
}

class ModelDuck extends Duck {
  constructor() {
    super();
    this.flyBehavior = new FlyNullObject();
    this.quackBehavior = new Quack();
  }

  display(): void {
    console.log("I'm a model duck");
  }
}

const model = new ModelDuck();
model.performFly();                          // does nothing
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();                          // "Flying with a rocket"

In languages with first-class functions, a strategy is often just a functionComparator<T> in Java (often written as a lambda like (a, b) -> a.getName().compareTo(b.getName())), a key function passed to Python’s sorted(key=...), a lambda passed to Array.prototype.sort. Use an explicit Strategy class when the algorithm needs identity, configuration data, multiple operations, polymorphic dispatch beyond a single call, or test seams.

Design Decisions

How does the Strategy access Context data?

When a Strategy needs information from the Context to do its job, there are two main approaches (Gamma et al. 1995):

  • Pass data as parameters: The Context passes everything the Strategy needs through the algorithm interface (e.g., compose(componentSizes, lineWidth, breaks)). This keeps Strategy and Context decoupled, but the Context may have to pass data the Strategy doesn’t actually need.
  • Pass the Context itself: The Context passes itself as an argument, and the Strategy queries the Context for whatever data it needs (e.g., strategy.execute(this)). This lets the Strategy ask for exactly what it wants but requires Context to expose a richer interface, increasing coupling.

The right choice depends on the algorithm’s data needs and how stable the Context’s interface is.

Compile-time vs. runtime strategy selection

  • Runtime selection (the standard form): the Strategy is held as a field and can be swapped via a setter. This enables dynamic reconfiguration — exactly what setFlyBehavior() enables in the duck example.
  • Compile-time selection (C++ template parameter, generics): the Strategy is bound when the type is instantiated — known as policy-based design in C++. This is more efficient (no virtual dispatch, possibly inlinable) but cannot change at runtime. Useful when the choice is fixed at configuration time and performance matters (Gamma et al. 1995).

Optional Strategy with default behavior

The Context can be simplified if it’s meaningful for the Strategy reference to be absent. The Context checks if a Strategy is set: if so, it delegates; if not, it falls back to a default behavior (Gamma et al. 1995). Clients that want the default never have to deal with Strategy objects at all. The Null Object variant (e.g., FlyNullObject) achieves the same effect more uniformly: a “do nothing” Strategy keeps the Context’s call site simple (flyBehavior.fly()) without null checks.

Stateless vs. stateful strategies

If a Strategy carries no instance data, it can be shared across many Contexts as a Flyweight or Singleton, saving memory and avoiding repeated allocation. If it carries per-Context configuration (e.g., a RangeValidator(min=0, max=100)), each Context needs its own Strategy instance.

Consequences

Applying the Strategy pattern yields several important consequences (Gamma et al. 1995):

  • Families of related algorithms. Strategy hierarchies define a family of interchangeable algorithms. Common functionality can be factored out via inheritance among ConcreteStrategies.
  • An alternative to subclassing. Rather than baking each algorithm variant into a Context subclass — which couples algorithm and Context tightly — Strategy encapsulates each algorithm separately. The Context becomes simpler, and algorithms can vary independently.
  • Eliminates conditional statements. Code with many if/switch branches selecting between algorithms is a strong code smell pointing to Strategy. Each branch becomes a polymorphic ConcreteStrategy. This is the polymorphism over conditions principle that also underlies the State pattern.
  • A choice of implementations. Strategies can provide different implementations of the same behavior with different time/space trade-offs (e.g., a fast approximate sort vs. a careful stable sort), letting the client choose.
  • Clients must know about the strategies. Because the client typically picks the ConcreteStrategy, it must understand how the strategies differ. If the choice should be hidden from clients, Strategy is the wrong tool.
  • Communication overhead. The Strategy interface is shared by all ConcreteStrategies. Some may not need all the data the interface passes, leading to wasted preparation in the Context.
  • Increased number of objects. Strategy adds one class per algorithm variant. Stateless strategies can be shared as flyweights to mitigate this.

Strategy vs. Related Patterns

Pattern Similarity Difference
State Identical UML structure: a Context delegates to an interface with multiple implementations. State: behavior changes implicitly via internal transitions (the Context — or the State objects themselves — switch states in response to operations). Strategy: behavior is explicitly selected by the client; strategies don’t know about each other (Freeman and Robson 2020).
Template Method Both let you vary parts of an algorithm. Template Method uses inheritance — the base class fixes the skeleton and subclasses override individual steps. Strategy uses composition — the entire algorithm is swapped via an external object (Gamma et al. 1995).
Command Both wrap behavior in an object behind a common interface. Command represents a request with a lifecycle (queue, log, undo). Strategy represents an algorithm choice — there is no request identity, no undo, no queuing.
Observer Both replace static coupling with dynamic delegation. Observer broadcasts state changes to many listeners. Strategy routes one operation to one chosen algorithm.
Decorator Both can add or change behavior via composition. Decorator wraps an object to add behavior while preserving its interface. Strategy replaces an algorithm entirely — there is no chain of wrappers.

A useful heuristic distinguishing Strategy from State: ask whether the client picks the implementation (Strategy) or whether the object’s own internal logic picks it (State). If a GumballMachine switches from NoQuarterState to HasQuarterState because the user inserted a coin, that’s State. If a sort routine accepts a Comparator parameter, that’s Strategy.

Pattern Compounds and Idioms

Strategy combines naturally with other patterns:

  • Strategy + Singleton / Flyweight: Stateless strategies (e.g., Quack, Squeak) carry behavior but no data. They can be implemented as singletons or shared as flyweights to avoid creating one instance per Context.
  • Null Strategy: A “do nothing” ConcreteStrategy (e.g., FlyNullObject, MuteQuack) replaces null checks in the Context with uniform polymorphic dispatch. This is the Null Object pattern superimposed on Strategy.
  • Strategy + Factory Method / Abstract Factory: A factory selects which ConcreteStrategy to instantiate based on configuration, environment, or feature flags — keeping the Context oblivious to selection logic.
  • Strategy in MVC: In the MVC compound pattern, the Controller is a Strategy used by the View. Swapping controllers (e.g., from an editing controller to a read-only controller) reconfigures input behavior without modifying the View.

Common Examples

Domain Strategy interface Concrete strategies
Sorting Comparator<T> natural order, by-field, custom rules
Validation Validator range check, regex match, length check, composed validators
Compression Compressor gzip, zip, lz4, no-op
Payment PaymentMethod credit card, PayPal, bank transfer, gift card
Authentication AuthStrategy password, OAuth, SSO, API key
Game AI BehaviorStrategy aggressive, defensive, patrol, idle
Text layout Compositor simple greedy, TeX optimal, fixed-width array
Pricing DiscountStrategy seasonal, member, bulk, no discount

Practical Guidance: When NOT to Use Strategy

Strategy is not free. Skip it when:

  • There is only one algorithm. A single concrete class with a single method is simpler. Don’t create an interface and subclass for a variant that doesn’t exist yet — that’s speculative abstraction.
  • The variants will never change at runtime and clients don’t care. A simple inheritance hierarchy or even a parameter switch may be clearer.
  • The strategies are trivial one-liners. A function or lambda is often enough; the boilerplate of a class hierarchy is unjustified.
  • The choice is genuinely a state machine. If “which algorithm” depends on what the object is currently doing, State is the right tool — the structure looks identical but the intent differs.

As with all design patterns, keep the Rule of Three in mind: don’t introduce Strategy until you have at least three concrete variants or a clear plan for runtime swapping. The simplest code is usually the smartest design.

Flashcards

Strategy Pattern Flashcards

Key concepts, design decisions, and trade-offs of the Strategy design pattern.

Difficulty: Basic

What is the intent of the Strategy pattern?

Difficulty: Intermediate

What problem does Strategy solve?

Difficulty: Intermediate

What core OO principle does Strategy embody?

Difficulty: Basic

What are the three roles in the Strategy pattern?

Difficulty: Advanced

How does Strategy differ from State? They have identical UML structures.

Difficulty: Advanced

How does Strategy differ from Template Method?

Difficulty: Intermediate

What is a Null Object Strategy, and why is it useful?

Difficulty: Intermediate

Why are conditional if/switch statements selecting between algorithms a code smell that suggests Strategy?

Difficulty: Advanced

What is the main drawback of Strategy that makes it unsuitable when the choice should be hidden from clients?

Difficulty: Intermediate

When should a Strategy be implemented as a Singleton or Flyweight?

Difficulty: Expert

Two ways the Context can give the Strategy access to its data — what are they, and what’s the trade-off?

Difficulty: Basic

Give three real-world examples of the Strategy pattern in everyday programming.

Difficulty: Advanced

Why does the SimUDuck example put fly() and quack() into Strategy interfaces instead of using Flyable and Quackable interfaces directly on each duck?

Difficulty: Basic

Strategy is also known by what alternate name in the GoF catalog?

Difficulty: Advanced

When should you NOT use Strategy?

Quiz

Strategy Pattern Quiz

Test your understanding of the Strategy pattern's structure, its composition-over-inheritance principle, and the often-confused boundary with the State pattern.

Difficulty: Basic

A team is designing an e-commerce checkout system. Customers can pay by credit card, PayPal, gift card, or bank transfer. The CTO wants to add support for cryptocurrency next quarter without modifying any existing checkout code. Which design best fits?

Correct Answer:
Difficulty: Advanced

Consider this UML structure: a Context class holds a reference to an interface, and several concrete classes implement that interface. The Context delegates an operation to the held implementation, which can be swapped via a setter. Both the State and Strategy patterns have exactly this structure. What actually distinguishes them?

Correct Answer:
Difficulty: Intermediate

Which of the following are valid reasons to use the Strategy pattern? Select all that apply.

Correct Answers:
Difficulty: Intermediate

In Head First Design Patterns’ SimUDuck example, a first attempt puts fly() and quack() directly on the Duck superclass. This is then refactored to use Flyable and Quackable interfaces. Why is the interface approach still considered inferior to a Strategy-based design?

Correct Answer:
Difficulty: Advanced

A Compositor interface defines compose(natural[], stretch[], shrink[], width, breaks[]). Three ConcreteStrategies implement it: SimpleCompositor (greedy), TeXCompositor (paragraph-optimal), and ArrayCompositor (fixed-width grids). The SimpleCompositor ignores the stretch and shrink arrays entirely. Which Strategy consequence does this illustrate?

Correct Answer:
Difficulty: Intermediate

A teammate writes:

class FlyNullObject implements FlyBehavior {
    public void fly() { /* do nothing */ }
}

Why is this preferable to leaving the flyBehavior field as null and writing if (flyBehavior != null) flyBehavior.fly(); in the Context?

Correct Answer:
Difficulty: Advanced

Which of the following common library mechanisms is NOT a use of the Strategy pattern?

Correct Answer:

Observer


Want hands-on practice? Try the Interactive Observer Pattern Tutorial — experience the pain of tight coupling first, then refactor into Observer step by step with live UML diagrams, debugging challenges, and quizzes.

Problem 

In software design, you frequently encounter situations where one object’s state changes, and several other objects need to be notified of this change so they can update themselves accordingly. As the Gang of Four (GoF — the four authors of Design Patterns (Gamma et al. 1995)) describe it, this is a common side-effect of partitioning a system into a collection of cooperating classes: you need to maintain consistency between related objects, but you don’t want to achieve that consistency by making the classes tightly coupled, because that reduces their reusability.

The classic motivating example (GoF Observer chapter) is a graphical user interface toolkit that separates presentation from the underlying application data: a spreadsheet view and a bar chart can both depict the same numerical data using different presentations. The two views don’t know about each other, yet they must behave as though they do — when the user edits a value in the spreadsheet, the bar chart must reflect the change immediately, and vice versa. There is no reason to limit the number of dependents to two; any number of different views may want to display the same data.

If the dependent objects constantly check the core object for changes (polling), it wastes valuable CPU cycles and resources. Conversely, if the core object is hard-coded to directly update all its dependent objects, the classes become tightly coupled. Every time you need to add or remove a dependent object, you have to modify the core object’s code, violating the Open/Closed Principle.

The core problem is: How can a one-to-many dependency between objects be maintained efficiently without making the objects tightly coupled?

Intent (GoF): “Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”

Also Known As: Dependents, Publish-Subscribe (the GoF Observer chapter explicitly lists both as alternative names; POSA1 (Buschmann et al. 1996) documents the related pattern under the name Publisher-Subscriber, with Observer and Dependents as aliases).

Context

The Observer pattern is highly applicable in scenarios requiring distributed event handling systems or highly decoupled architectures. Common contexts include:

  • User Interfaces (GUI): A classic example is the Model-View-Controller (MVC) architecture. When the underlying data (Model) changes, multiple UI components (Views) like charts, tables, or text fields must update simultaneously to reflect the new data.

  • Event Management Systems: Applications that rely on events—such as user button clicks, incoming network requests, or file system changes—where an unknown number of listeners might want to react to a single event.

  • Social Media/News Feeds: A system where users (observers) follow a specific creator (subject) and need to be notified instantly when new content is posted.

Solution

The Observer design pattern solves this by establishing a one-to-many subscription mechanism.

It introduces two main roles: the Subject (the object sending updates after it has changed) and the Observer (the object listening to the updates of Subjects).

Instead of objects polling the Subject or the Subject being hard-wired to specific objects, the Subject maintains a dynamic list of Observers. It provides an interface for Observers to attach and detach themselves at runtime. When the Subject’s state changes, it iterates through its list of attached Observers and calls a specific notification method (e.g., update()) defined in the Observer interface.

This creates a loosely coupled system: the Subject only knows that its Observers implement a specific interface, not their concrete implementation details.

UML Role Diagram

UML Example Diagram

Sequence Diagram

This pattern is fundamentally about runtime collaboration, so a sequence diagram is helpful here.

Code Example

This sample implements the pull-style News Channel example from the diagrams. The subject sends a simple notification; each observer asks the subject for the latest post.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

import java.util.ArrayList;
import java.util.List;

interface Subscriber {
    void update();
}

final class NewsChannel {
    private final List<Subscriber> subscribers = new ArrayList<>();
    private String latestPost = "";

    void follow(Subscriber subscriber) {
        subscribers.add(subscriber);
    }

    void unfollow(Subscriber subscriber) {
        subscribers.remove(subscriber);
    }

    void publishPost(String text) {
        latestPost = text;
        subscribers.forEach(Subscriber::update);
    }

    String getLatestPost() {
        return latestPost;
    }
}

final class MobileApp implements Subscriber {
    private final NewsChannel channel;

    MobileApp(NewsChannel channel) {
        this.channel = channel;
    }

    public void update() {
        System.out.println("[MobileApp] " + channel.getLatestPost());
    }
}

final class EmailDigest implements Subscriber {
    private final NewsChannel channel;

    EmailDigest(NewsChannel channel) {
        this.channel = channel;
    }

    public void update() {
        System.out.println("[EmailDigest] " + channel.getLatestPost());
    }
}

public class Demo {
    public static void main(String[] args) {
        NewsChannel channel = new NewsChannel();
        Subscriber app = new MobileApp(channel);
        Subscriber email = new EmailDigest(channel);
        channel.follow(app);
        channel.follow(email);
        channel.publishPost("New video uploaded!");
        channel.unfollow(email);
        channel.publishPost("Live stream starting!");
    }
}
#include <algorithm>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

struct Subscriber {
    virtual ~Subscriber() = default;
    virtual void update() = 0;
};

class NewsChannel {
public:
    void follow(Subscriber& subscriber) {
        subscribers_.push_back(&subscriber);
    }

    void unfollow(Subscriber& subscriber) {
        subscribers_.erase(
            std::remove(subscribers_.begin(), subscribers_.end(), &subscriber),
            subscribers_.end());
    }

    void publishPost(std::string text) {
        latestPost_ = std::move(text);
        for (auto* subscriber : subscribers_) {
            subscriber->update();
        }
    }

    const std::string& latestPost() const {
        return latestPost_;
    }

private:
    std::vector<Subscriber*> subscribers_;
    std::string latestPost_;
};

class MobileApp : public Subscriber {
public:
    explicit MobileApp(const NewsChannel& channel) : channel_(channel) {}

    void update() override {
        std::cout << "[MobileApp] " << channel_.latestPost() << "\n";
    }

private:
    const NewsChannel& channel_;
};

class EmailDigest : public Subscriber {
public:
    explicit EmailDigest(const NewsChannel& channel) : channel_(channel) {}

    void update() override {
        std::cout << "[EmailDigest] " << channel_.latestPost() << "\n";
    }

private:
    const NewsChannel& channel_;
};

int main() {
    NewsChannel channel;
    MobileApp app(channel);
    EmailDigest email(channel);
    channel.follow(app);
    channel.follow(email);
    channel.publishPost("New video uploaded!");
    channel.unfollow(email);
    channel.publishPost("Live stream starting!");
}
from abc import ABC, abstractmethod


class Subscriber(ABC):
    @abstractmethod
    def update(self) -> None:
        pass


class NewsChannel:
    def __init__(self) -> None:
        self._subscribers: list[Subscriber] = []
        self._latest_post = ""

    def follow(self, subscriber: Subscriber) -> None:
        self._subscribers.append(subscriber)

    def unfollow(self, subscriber: Subscriber) -> None:
        self._subscribers.remove(subscriber)

    def publish_post(self, text: str) -> None:
        self._latest_post = text
        for subscriber in self._subscribers:
            subscriber.update()

    def get_latest_post(self) -> str:
        return self._latest_post


class MobileApp(Subscriber):
    def __init__(self, channel: NewsChannel) -> None:
        self._channel = channel

    def update(self) -> None:
        print(f"[MobileApp] {self._channel.get_latest_post()}")


class EmailDigest(Subscriber):
    def __init__(self, channel: NewsChannel) -> None:
        self._channel = channel

    def update(self) -> None:
        print(f"[EmailDigest] {self._channel.get_latest_post()}")


channel = NewsChannel()
app = MobileApp(channel)
email = EmailDigest(channel)
channel.follow(app)
channel.follow(email)
channel.publish_post("New video uploaded!")
channel.unfollow(email)
channel.publish_post("Live stream starting!")
interface Subscriber {
  update(): void;
}

class NewsChannel {
  private subscribers: Subscriber[] = [];
  private latestPost = "";

  follow(subscriber: Subscriber): void {
    this.subscribers.push(subscriber);
  }

  unfollow(subscriber: Subscriber): void {
    this.subscribers = this.subscribers.filter((item) => item !== subscriber);
  }

  publishPost(text: string): void {
    this.latestPost = text;
    this.subscribers.forEach((subscriber) => subscriber.update());
  }

  getLatestPost(): string {
    return this.latestPost;
  }
}

class MobileApp implements Subscriber {
  constructor(private readonly channel: NewsChannel) {}

  update(): void {
    console.log(`[MobileApp] ${this.channel.getLatestPost()}`);
  }
}

class EmailDigest implements Subscriber {
  constructor(private readonly channel: NewsChannel) {}

  update(): void {
    console.log(`[EmailDigest] ${this.channel.getLatestPost()}`);
  }
}

const channel = new NewsChannel();
const app = new MobileApp(channel);
const email = new EmailDigest(channel);
channel.follow(app);
channel.follow(email);
channel.publishPost("New video uploaded!");
channel.unfollow(email);
channel.publishPost("Live stream starting!");

Design Decisions

Push vs. Pull Model

This is the most important design decision when tailoring the Observer pattern.

Push Model: The Subject sends the detailed state information to the Observer as arguments in the update() method, even if the Observer doesn’t need all data. The Observer doesn’t need a reference back to the Subject, but it does become coupled to the Subject’s data format — which can compromise Observer reusability across different Subjects. It can also be inefficient if large data is passed unnecessarily. Use this when all observers need the same data, or when the Subject’s interface should remain hidden from observers.

Pull Model: The Subject sends a minimal notification, and the Observer is responsible for querying the Subject for the specific data it needs. This requires the Observer to have a reference back to the Subject, slightly increasing coupling. It can be more efficient than push when different observers need different subsets of data (each pulls only what it uses), but less efficient when every observer would consume the same payload that push could deliver in one call. Use this when different observers need different subsets of data, or when the data is expensive to compute and not all observers will use it.

Hybrid Model: The Subject pushes the type of change (e.g., an event enum or change descriptor), and observers decide whether to pull additional data based on the event type. This balances decoupling with efficiency and is the most common approach in modern frameworks.

Observer Lifecycle: The Lapsed Listener Problem

A critical but often overlooked decision is how observer registrations are managed over time. If an observer registers with a subject but is never explicitly detached, the subject’s reference list keeps the observer alive in memory—even after the observer is otherwise unused. This is the lapsed listener problem, a common source of memory leaks. Solutions include:

  • Explicit unsubscribe: Require observers to detach themselves (disciplined but error-prone).
  • Weak references: The subject holds weak references to observers, allowing garbage collection (language-dependent).
  • Scoped subscriptions: Tie the observer’s registration to a lifecycle scope that automatically unsubscribes on cleanup (common in modern UI frameworks).

Notification Trigger

Who triggers the notification? GoF (Implementation issue #3, “Who triggers the update?”) frames the same trade-off, listing two options; modern practice adds a third:

  • Automatic: The Subject’s setter methods call notifyObservers() after every state change. Simple — clients don’t have to remember to call notify — but consecutive state changes cause consecutive notifications, which may be inefficient.
  • Client-triggered: The client explicitly calls notifyObservers() after making all desired changes. The client can wait until a series of state changes is complete, avoiding needless intermediate updates, but clients carry the responsibility and may forget.
  • Batched/deferred: Notifications are collected and dispatched after a delay or at a synchronization point, reducing redundant updates.

Self-Consistency Before Notification

GoF (Implementation issue #5) warns that a Subject must be in a self-consistent state before calling notify, because observers will query the subject for its current state during their update. This is easy to violate when a subclass operation calls an inherited operation that triggers the notification before the subclass has finished its own state update. A standard fix is to send notifications from a Template Method in the abstract Subject — define a primitive operation for subclasses to override, and make Notify() the last step of the template method, so the object is guaranteed to be self-consistent when subclasses override Subject operations.

Observing Multiple Subjects

GoF (Implementation issue #2) notes that an observer may depend on more than one subject (e.g., a spreadsheet cell that draws from several data sources). In that case, the update() operation needs to tell the observer which subject changed — typically by passing the subject as a parameter (update(Subject* changedSubject)). The pull style naturally supports this; a pure push style with no subject identity makes it harder.

Dangling References to Deleted Subjects

GoF (Implementation issue #4) flags a subtle ownership bug: if a subject is deleted while observers still hold references to it, those references dangle. One remedy is to have the subject notify its observers as it is destroyed, so they can null out their references. This is the dual of the lapsed-listener problem above and matters most in languages without garbage collection.

Specifying Modifications of Interest (Aspects)

GoF (Implementation issue #7) discusses extending the registration interface so observers can subscribe only to specific events of interest (e.g., Subject::Attach(Observer*, Aspect& interest)). This avoids waking up every observer on every change and is the conceptual ancestor of typed event handlers in modern frameworks (e.g., separate listener interfaces per event type, or topic-based publish-subscribe).

Encapsulating Complex Update Semantics (ChangeManager)

When the dependency graph between subjects and observers is intricate — e.g., observers depend on multiple subjects and you must avoid duplicate updates when several change at once — GoF (Implementation issue #9) recommends introducing a separate ChangeManager object that maps subjects to observers, defines an update strategy, and dispatches updates on the subject’s behalf. GoF cite two specializations: a SimpleChangeManager that always updates every observer, and a DAGChangeManager that handles directed acyclic graphs of dependencies and ensures each observer is updated only once per change event. The ChangeManager is itself an instance of the Mediator pattern and is typically a Singleton.

Consequences

Applying the Observer pattern yields several important consequences. The first three are the canonical GoF benefits (Consequences §1–§3); the remaining items capture liabilities GoF flag and one widely observed comprehension issue.

  • Abstract coupling between Subject and Observer (loose coupling): The subject knows only that its observers conform to a simple interface — not their concrete classes. Because Subject and Observer aren’t tightly coupled, they can also belong to different layers of abstraction in the system: a lower-level subject can notify a higher-level observer without violating the layering.
  • Support for broadcast communication: Unlike an ordinary request, the notification a subject sends needn’t specify its receiver — it is broadcast automatically to every observer that subscribed. The subject doesn’t care how many interested objects exist; it is up to each observer to handle or ignore a notification.
  • Dynamic Relationships: Observers can be added and removed at any time during execution, enabling highly flexible architectures.
  • Unexpected updates: Because observers have no knowledge of each other’s presence, a seemingly innocuous operation on the subject can cause a cascade of updates to observers and their dependent objects. The simple update() protocol carries no information about what changed, so observers may have to work hard to deduce the changes — a frequent source of subtle bugs that are hard to track down.
  • Inverted dependency flow makes comprehension harder: Conceptually, data flows from subject to observer, but in the code the observer calls the subject to register itself. When a reader encounters an observer for the first time, there is no sign near the observer of what it depends on — the wiring lives elsewhere. This inversion is widely cited as a comprehension hazard for Observer-based systems and is one reason modern reactive frameworks try to make the dependency graph explicit at the call site.

Known Uses

GoF cite the following examples; the pattern is far more pervasive today, but these are the historical anchors:

  • Smalltalk Model/View/Controller (MVC): the first and best-known use. Smalltalk’s Model plays the role of Subject and View is the base class for observers. Smalltalk, ET++, and the THINK class library put Subject and Observer interfaces in the root class Object, making the dependency mechanism available to every object in the system.
  • InterViews, the Andrew Toolkit, and Unidraw all employ the pattern in their UI frameworks. InterViews defines Observer and Observable classes explicitly; Andrew calls them “view” and “data object”; Unidraw splits graphical editor objects into View (observers) and Subject parts.
  • Java’s standard library: java.util.Observer / java.util.Observable provided a built-in implementation. Caveat for modern code: both have since been deprecated in modern JDKs because Observable is a class (forcing single inheritance) with protected methods that require subclassing rather than composition — Head First Design Patterns’ “dark side of java.util.Observable” section in Chapter 2 lays out exactly these criticisms. Modern Java code typically uses java.beans.PropertyChangeListener, the Flow API publishers, or a third-party reactive library instead.
  • Swing and JavaBeans: the listener model in JButton/AbstractButton (addActionListener, etc.) is a typed-event variant of Observer; PropertyChangeListener plays a similar role at the bean level.

Related Patterns

  • Mediator: GoF note that the ChangeManager described under Implementation is itself a Mediator — it sits between subjects and observers and encapsulates complex update semantics so neither side has to know about the other directly.
  • Singleton: A ChangeManager is typically unique and globally accessible, making Singleton a natural choice for its lifecycle.
  • Template Method: A common technique for keeping subjects self-consistent before notifying (Implementation issue #5) is to put Notify() as the final step of a template method in the abstract Subject, with the state-changing primitive operation overridden in subclasses.
  • POSA1’s Publisher-Subscriber: documents the same pattern at a coarser, architectural granularity — for example as a Gatekeeper or as an Event Channel between processes — and is the conceptual root of message-broker and pub/sub middleware.

Factory Method


Context

In software construction, we often find ourselves in situations where a “Creator” class needs to manage a lifecycle of actions—such as preparing, processing, and delivering an item—but the specific type of item it handles varies based on the environment.

For example, imagine a PizzaStore that needs to orderPizza(). The store follows a standard process: it must prepare(), bake(), cut(), and box() the pizza. However, the specific type of pizza (New York style vs. Chicago style) depends on the store’s physical location. The “Context” here is a system where the high-level process is stable, but the specific objects being acted upon are volatile and vary based on concrete subclasses.

Problem

Without a creational pattern, developers often resort to “Big Upfront Logic” using complex conditional statements. You might see code like this:

public Pizza orderPizza(String type) {
    Pizza pizza;
    if (type.equals("cheese")) { pizza = new CheesePizza(); }
    else if (type.equals("greek")) { pizza = new GreekPizza(); }
    // ... more if-else blocks ...
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

This approach presents several critical challenges:

  1. Violation of Single Responsibility Principle: This single method is now responsible for both deciding which pizza to create and managing the baking process.
  2. Divergent Change: Every time the menu changes or the baking process is tweaked, this method must be modified, making it a “hot spot” for bugs.
  3. Tight Coupling: The store is “intimately” aware of every concrete pizza class, making it impossible to add new regional styles without rewriting the store’s core logic.

Solution

The Factory Method Pattern solves this by defining an interface for creating an object but letting subclasses decide which class to instantiate. It effectively “defers” the responsibility of creation to subclasses.

In our PizzaStore example, we typically make the createPizza() method abstract within the base PizzaStore class. This abstract method is the “Factory Method”. We then create concrete subclasses like NYPizzaStore and ChicagoPizzaStore, each implementing createPizza() to return their specific regional variants. (GoF also allows the Creator to provide a default implementation that subclasses may optionally override — see Abstract vs. Concrete Creator below.)

The structure involves four key roles (using GoF’s names; the parenthesized names are from the GoF Application/Document motivating example):

  • Product (Document): defines the interface of objects the factory method creates (e.g., Pizza). This can be a Java interface or an abstract class — both are valid; Head First uses an abstract Pizza class with default prepare()/bake()/cut()/box() implementations that subclasses can override.
  • ConcreteProduct (MyDocument): implements the Product interface (e.g., NYStyleCheesePizza).
  • Creator (Application): declares the factory method, which returns an object of type Product. May also define a default implementation that returns a default ConcreteProduct. May also call the factory method to create a Product (often inside a Template Method, in GoF terminology — in our example, orderPizza() is the template method that calls createPizza()).
  • ConcreteCreator (MyApplication): overrides the factory method to return an instance of a ConcreteProduct (e.g., NYPizzaStore returns NYStyleCheesePizza).

Factory Method vs. “Simple Factory”: A common point of confusion is the Simple Factory (sometimes called Static Factory Method) — a single non-abstract class with a parameterized method (typically a chain of if/else or a switch) that returns one of several product types. Head First Design Patterns gives Simple Factory only an “honorable mention”, noting it is a programming idiom rather than a true design pattern. The GoF Factory Method differs in that it defers instantiation to subclasses via inheritance — each ConcreteCreator overrides the factory method, rather than one factory class switching on a type parameter.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

The base PizzaStore owns the stable ordering algorithm. The factory method, createPizza, is the one step subclasses vary.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface Pizza {
    void prepare();
    void bake();
    void cut();
    void box();
}

final class NYStyleCheesePizza implements Pizza {
    public void prepare() {
        System.out.println("Preparing NY cheese pizza");
    }

    public void bake() {
        System.out.println("Baking thin crust");
    }

    public void cut() {
        System.out.println("Cutting into diagonal slices");
    }

    public void box() {
        System.out.println("Boxing in NY PizzaStore box");
    }
}

abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }

    protected abstract Pizza createPizza(String type);
}

final class NYPizzaStore extends PizzaStore {
    protected Pizza createPizza(String type) {
        if (!type.equals("cheese")) {
            throw new IllegalArgumentException("Unknown pizza: " + type);
        }
        return new NYStyleCheesePizza();
    }
}

public class Demo {
    public static void main(String[] args) {
        PizzaStore store = new NYPizzaStore();
        store.orderPizza("cheese");
    }
}
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>

struct Pizza {
    virtual ~Pizza() = default;
    virtual void prepare() = 0;
    virtual void bake() = 0;
    virtual void cut() = 0;
    virtual void box() = 0;
};

struct NYStyleCheesePizza : Pizza {
    void prepare() override { std::cout << "Preparing NY cheese pizza\n"; }
    void bake() override { std::cout << "Baking thin crust\n"; }
    void cut() override { std::cout << "Cutting into diagonal slices\n"; }
    void box() override { std::cout << "Boxing in NY PizzaStore box\n"; }
};

class PizzaStore {
public:
    virtual ~PizzaStore() = default;

    std::unique_ptr<Pizza> orderPizza(const std::string& type) {
        auto pizza = createPizza(type);
        pizza->prepare();
        pizza->bake();
        pizza->cut();
        pizza->box();
        return pizza;
    }

protected:
    virtual std::unique_ptr<Pizza> createPizza(const std::string& type) = 0;
};

class NYPizzaStore : public PizzaStore {
protected:
    std::unique_ptr<Pizza> createPizza(const std::string& type) override {
        if (type != "cheese") throw std::invalid_argument("unknown pizza");
        return std::make_unique<NYStyleCheesePizza>();
    }
};

int main() {
    NYPizzaStore store;
    auto pizza = store.orderPizza("cheese");
}
from abc import ABC, abstractmethod


class Pizza(ABC):
    @abstractmethod
    def prepare(self) -> None:
        pass

    @abstractmethod
    def bake(self) -> None:
        pass

    @abstractmethod
    def cut(self) -> None:
        pass

    @abstractmethod
    def box(self) -> None:
        pass


class NYStyleCheesePizza(Pizza):
    def prepare(self) -> None:
        print("Preparing NY cheese pizza")

    def bake(self) -> None:
        print("Baking thin crust")

    def cut(self) -> None:
        print("Cutting into diagonal slices")

    def box(self) -> None:
        print("Boxing in NY PizzaStore box")


class PizzaStore(ABC):
    def order_pizza(self, kind: str) -> Pizza:
        pizza = self.create_pizza(kind)
        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()
        return pizza

    @abstractmethod
    def create_pizza(self, kind: str) -> Pizza:
        pass


class NYPizzaStore(PizzaStore):
    def create_pizza(self, kind: str) -> Pizza:
        if kind != "cheese":
            raise ValueError(f"Unknown pizza: {kind}")
        return NYStyleCheesePizza()


store = NYPizzaStore()
store.order_pizza("cheese")
interface Pizza {
  prepare(): void;
  bake(): void;
  cut(): void;
  box(): void;
}

class NYStyleCheesePizza implements Pizza {
  prepare(): void {
    console.log("Preparing NY cheese pizza");
  }

  bake(): void {
    console.log("Baking thin crust");
  }

  cut(): void {
    console.log("Cutting into diagonal slices");
  }

  box(): void {
    console.log("Boxing in NY PizzaStore box");
  }
}

abstract class PizzaStore {
  orderPizza(kind: string): Pizza {
    const pizza = this.createPizza(kind);
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
  }

  protected abstract createPizza(kind: string): Pizza;
}

class NYPizzaStore extends PizzaStore {
  protected createPizza(kind: string): Pizza {
    if (kind !== "cheese") throw new Error(`Unknown pizza: ${kind}`);
    return new NYStyleCheesePizza();
  }
}

const store = new NYPizzaStore();
store.orderPizza("cheese");

Consequences

The primary benefit of this pattern is decoupling: the high-level “Creator” code is completely oblivious to which “Concrete Product” it is actually using. This allows the system to evolve independently; you can add a LAPizzaStore without touching a single line of code in the original PizzaStore base class. As GoF puts it, factory methods eliminate the need to bind application-specific classes into your code.

GoF also calls out two further consequences worth highlighting:

  • Provides hooks for subclasses. Creating an object inside a class with a factory method is always more flexible than creating an object directly with new. Even when the base creator provides a reasonable default, the factory method gives subclasses a hook to override the kind of object created.
  • Connects parallel class hierarchies. When a class delegates a responsibility to a separate hierarchy (e.g., FigureManipulator in GoF’s example), a factory method on one side localizes the knowledge of which class on the other side belongs with which.

However, there are trade-offs:

  • Forced subclassing. Clients may have to subclass Creator just to instantiate a particular ConcreteProduct. Subclassing is fine when the client was going to subclass anyway — otherwise it adds another point of evolution. (This is the motivating reason GoF discusses the Using templates to avoid subclassing and Parameterized factory methods variants in Implementation.)
  • Boilerplate Code: It requires creating many new classes (one for each product type and one for each creator type), which can increase the “static” complexity of the code.
  • Program Comprehension: While it reduces long-term maintenance costs, it can make the initial learning curve steeper for new developers who aren’t familiar with the pattern.

Design Decisions

Abstract vs. Concrete Creator

  • Abstract Creator (as shown above): Forces every subclass to implement the factory method. Maximum flexibility, but requires subclassing even for simple cases.
  • Concrete Creator with default: The base creator provides a default product. Subclasses only override when they need a different product. Simpler, but may lead to confusion about when overriding is expected.

Parameterized Factory Method

A single factory method can take a parameter (like a String or enum) that identifies the kind of object to create — all variants share the same Product interface. Our example uses this form (createPizza("cheese")). GoF presents this as a variation of Factory Method, not a replacement: subclasses can still override the parameterized method to add new identifiers (e.g., a MyCreator::Create that handles new IDs and falls through to Creator::Create for the rest). It does shift conditional logic into a switch on the type parameter, so naive non-overriding implementations — adding cases by editing the existing method — violate the Open/Closed Principle. The polymorphic-override usage does not.

Using Templates to Avoid Subclassing (C++)

GoF also notes that in C++ you can use templates to avoid the subclass-just-to-pick-a-Product problem: a template <class TheProduct> class StandardCreator : public Creator { Product* CreateProduct() { return new TheProduct; } }; lets the client supply the product class with no Creator subclass at all. Modern Java/C# generics support a similar pattern.

Static Factory Method (Not GoF)

A common idiom—Loan.newTermLoan()—uses static methods on the product class itself to control creation. This is not the GoF Factory Method (which relies on subclass override), but is widely used in practice. It provides named constructors and can return cached instances or subtype variants.

Language-specific Variants

GoF discusses language-specific implementation details:

  • C++: factory methods are typically virtual (often pure virtual). Don’t call them from the Creator’s constructor — the ConcreteCreator’s override won’t be available yet. Lazy initialization via an accessor (GetProduct()) that calls CreateProduct() on first use is one workaround.
  • Smalltalk / dynamically-typed languages: factory methods can return a class (not an instance), giving even later binding for the type of ConcreteProduct.
  • Naming conventions: GoF cites MacApp’s convention of declaring abstract factory methods as Class* DoMakeClass() to make their role obvious.

Choosing the Right Creational Pattern

A common source of confusion is when to use Factory Method vs. the other creational patterns. The key discriminators are:

Pattern Use When… Key Characteristic
Factory Method Only one type of product; subclasses decide which concrete type Simplest; uses inheritance (subclass overrides a method)
Abstract Factory A family of multiple related product types that must work together Uses composition (client receives a factory object); highest extensibility for new families
Builder Product has many parts with sequential construction; construction process itself varies Separates the construction algorithm from the object representation

An important insight: factory methods often lurk inside Abstract Factories. Each creation method in an Abstract Factory (e.g., createDough(), createSauce()) is itself a factory method. The Abstract Factory defines the interface; the concrete factory subclasses implement each method—which is exactly the Factory Method pattern applied to multiple products.

Related Patterns

GoF connects Factory Method to several other patterns:

  • Abstract Factory is often implemented with factory methods. The motivating example in Abstract Factory illustrates Factory Method as well.
  • Template Method typically calls factory methods. In our PizzaStore, orderPizza() is a template method (the fixed prepare → bake → cut → box sequence) that delegates the one varying step to the createPizza() factory method.
  • Prototype doesn’t require subclassing the Creator (you supply a prototypical instance to clone instead). However, it often requires an Initialize operation on the Product class — Factory Method doesn’t.

Flashcards

Factory Method & Abstract Factory Flashcards

Key concepts and comparisons for creational design patterns.

Difficulty: Intermediate

What problem does Factory Method solve?

Difficulty: Basic

What are the four roles in Factory Method?

Difficulty: Intermediate

Factory Method vs. Abstract Factory: when to use which?

Difficulty: Advanced

What is a parameterized factory method?

Difficulty: Advanced

How does Factory Method relate to Abstract Factory?

Difficulty: Advanced

What is the ‘Rigid Interface’ drawback of Abstract Factory?

Difficulty: Intermediate

Abstract Factory uses __ ; Factory Method uses __.

Quiz

Factory Method & Abstract Factory Quiz

Test your understanding of creational patterns — when to use which, design decisions, and their relationships.

Difficulty: Intermediate

A PizzaStore uses a parameterized factory method: createPizza(String type) with an if/else chain to decide which pizza to create. A new pizza type (“BBQ Chicken”) must be added. What is the design problem?

Correct Answer:
Difficulty: Intermediate

A system needs to create families of related UI components (Button, TextField, Checkbox) that must be visually consistent — all from the same theme (Material, iOS, Windows). Which pattern is most appropriate?

Correct Answer:
Difficulty: Advanced

A common shorthand contrasts Factory Method and Abstract Factory along an inheritance-vs-composition axis. What does that contrast mean structurally?

Correct Answer:
Difficulty: Advanced

An Abstract Factory interface has 12 creation methods (one per product type). A new product type must be added. What is the consequence?

Correct Answer:
Difficulty: Advanced

Each method in a PizzaIngredientFactorycreateDough(), createSauce(), createCheese() — is implemented differently by NYPizzaIngredientFactory and ChicagoPizzaIngredientFactory. What is the relationship between these creation methods and the Factory Method pattern?

Correct Answer:

Abstract Factory


Context

In complex software systems, we often encounter situations where we must manage multiple categories of related objects that need to work together consistently. Imagine a software framework for a pizza franchise that has expanded into different regions, such as New York and Chicago. Each region has its own specific set of ingredients: New York uses thin crust dough and Marinara sauce, while Chicago uses thick crust dough and plum tomato sauce. The high-level process of preparing a pizza remains stable across all locations, but the specific “family” of ingredients used depends entirely on the geographical context.

Problem

The primary challenge arises when a system needs to be independent of how its products are created, but those products belong to families that must be used together. Without a formal creational pattern, developers might encounter the following issues:

  • Inconsistent Product Groupings: There is a risk that a “rogue” franchise might accidentally mix New York thin crust with Chicago plum-tomato sauce, leading to a product that doesn’t meet quality standards.
  • Parallel Inheritance Hierarchies: You often end up with multiple hierarchies (e.g., a Dough hierarchy, a Sauce hierarchy, and a Cheese hierarchy) that all need to be instantiated based on the same single decision point, such as the region.
  • Tight Coupling: If the Pizza class directly instantiates concrete ingredient classes, it becomes “intimate” with every regional variation, making it incredibly difficult to add a new region like Los Angeles without modifying existing code.

Solution

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. Note: Some sources call this a “factory of factories”, but that shorthand is misleading: an Abstract Factory does not literally produce other factory objects—it produces product objects via factory objects. A much better mental model is to think of it as a “Product Family Factory” or an “Ingredients Factory”. Structurally, a single Abstract Factory interface contains a collection of operations that fit the Factory Method shape—one for each product in the family.

The design pattern involves these roles:

  1. Abstract Factory Interface: Defining an interface (e.g., PizzaIngredientFactory) with a creation method for each type of product in the family (e.g., createDough(), createSauce()).
  2. Concrete Factories: Implementing regional subclasses (e.g., NYPizzaIngredientFactory) that produce the specific variants of those products.
  3. Client: The client (e.g., the Pizza class) no longer knows about specific ingredients. Instead, it is passed an IngredientFactory and simply asks for its components, remaining completely oblivious to whether it is receiving New York or Chicago variants.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

This example keeps the client (CheesePizza) independent of concrete ingredient classes. Switching from New York to Chicago means passing a different factory object, not rewriting the pizza.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface Dough { String name(); }
interface Sauce { String name(); }
interface Cheese { String name(); }

final class ThinCrustDough implements Dough {
    public String name() { return "thin crust dough"; }
}

final class MarinaraSauce implements Sauce {
    public String name() { return "marinara sauce"; }
}

final class ReggianoCheese implements Cheese {
    public String name() { return "reggiano cheese"; }
}

interface PizzaIngredientFactory {
    Dough createDough();
    Sauce createSauce();
    Cheese createCheese();
}

final class NYPizzaIngredientFactory implements PizzaIngredientFactory {
    public Dough createDough() { return new ThinCrustDough(); }
    public Sauce createSauce() { return new MarinaraSauce(); }
    public Cheese createCheese() { return new ReggianoCheese(); }
}

final class CheesePizza {
    private final PizzaIngredientFactory factory;

    CheesePizza(PizzaIngredientFactory factory) {
        this.factory = factory;
    }

    void prepare() {
        Dough dough = factory.createDough();
        Sauce sauce = factory.createSauce();
        Cheese cheese = factory.createCheese();
        System.out.println("Preparing pizza with "
            + dough.name() + ", " + sauce.name() + ", " + cheese.name());
    }
}

public class Demo {
    public static void main(String[] args) {
        CheesePizza pizza = new CheesePizza(new NYPizzaIngredientFactory());
        pizza.prepare();
    }
}
#include <iostream>
#include <memory>
#include <string>

struct Dough { virtual ~Dough() = default; virtual std::string name() const = 0; };
struct Sauce { virtual ~Sauce() = default; virtual std::string name() const = 0; };
struct Cheese { virtual ~Cheese() = default; virtual std::string name() const = 0; };

struct ThinCrustDough : Dough {
    std::string name() const override { return "thin crust dough"; }
};

struct MarinaraSauce : Sauce {
    std::string name() const override { return "marinara sauce"; }
};

struct ReggianoCheese : Cheese {
    std::string name() const override { return "reggiano cheese"; }
};

struct PizzaIngredientFactory {
    virtual ~PizzaIngredientFactory() = default;
    virtual std::unique_ptr<Dough> createDough() const = 0;
    virtual std::unique_ptr<Sauce> createSauce() const = 0;
    virtual std::unique_ptr<Cheese> createCheese() const = 0;
};

struct NYPizzaIngredientFactory : PizzaIngredientFactory {
    std::unique_ptr<Dough> createDough() const override {
        return std::make_unique<ThinCrustDough>();
    }
    std::unique_ptr<Sauce> createSauce() const override {
        return std::make_unique<MarinaraSauce>();
    }
    std::unique_ptr<Cheese> createCheese() const override {
        return std::make_unique<ReggianoCheese>();
    }
};

class CheesePizza {
public:
    explicit CheesePizza(const PizzaIngredientFactory& factory)
        : factory_(factory) {}

    void prepare() const {
        auto dough = factory_.createDough();
        auto sauce = factory_.createSauce();
        auto cheese = factory_.createCheese();
        std::cout << "Preparing pizza with " << dough->name()
                  << ", " << sauce->name() << ", " << cheese->name() << "\n";
    }

private:
    const PizzaIngredientFactory& factory_;
};

int main() {
    NYPizzaIngredientFactory factory;
    CheesePizza pizza(factory);
    pizza.prepare();
}
from abc import ABC, abstractmethod


class Dough(ABC):
    @abstractmethod
    def name(self) -> str:
        pass


class Sauce(ABC):
    @abstractmethod
    def name(self) -> str:
        pass


class Cheese(ABC):
    @abstractmethod
    def name(self) -> str:
        pass


class ThinCrustDough(Dough):
    def name(self) -> str:
        return "thin crust dough"


class MarinaraSauce(Sauce):
    def name(self) -> str:
        return "marinara sauce"


class ReggianoCheese(Cheese):
    def name(self) -> str:
        return "reggiano cheese"


class PizzaIngredientFactory(ABC):
    @abstractmethod
    def create_dough(self) -> Dough:
        pass

    @abstractmethod
    def create_sauce(self) -> Sauce:
        pass

    @abstractmethod
    def create_cheese(self) -> Cheese:
        pass


class NYPizzaIngredientFactory(PizzaIngredientFactory):
    def create_dough(self) -> Dough:
        return ThinCrustDough()

    def create_sauce(self) -> Sauce:
        return MarinaraSauce()

    def create_cheese(self) -> Cheese:
        return ReggianoCheese()


class CheesePizza:
    def __init__(self, factory: PizzaIngredientFactory) -> None:
        self.factory = factory

    def prepare(self) -> None:
        dough = self.factory.create_dough()
        sauce = self.factory.create_sauce()
        cheese = self.factory.create_cheese()
        print(f"Preparing pizza with {dough.name()}, {sauce.name()}, {cheese.name()}")


pizza = CheesePizza(NYPizzaIngredientFactory())
pizza.prepare()
interface Dough { name(): string; }
interface Sauce { name(): string; }
interface Cheese { name(): string; }

class ThinCrustDough implements Dough {
  name(): string { return "thin crust dough"; }
}

class MarinaraSauce implements Sauce {
  name(): string { return "marinara sauce"; }
}

class ReggianoCheese implements Cheese {
  name(): string { return "reggiano cheese"; }
}

interface PizzaIngredientFactory {
  createDough(): Dough;
  createSauce(): Sauce;
  createCheese(): Cheese;
}

class NYPizzaIngredientFactory implements PizzaIngredientFactory {
  createDough(): Dough { return new ThinCrustDough(); }
  createSauce(): Sauce { return new MarinaraSauce(); }
  createCheese(): Cheese { return new ReggianoCheese(); }
}

class CheesePizza {
  constructor(private readonly factory: PizzaIngredientFactory) {}

  prepare(): void {
    const dough = this.factory.createDough();
    const sauce = this.factory.createSauce();
    const cheese = this.factory.createCheese();
    console.log(`Preparing pizza with ${dough.name()}, ${sauce.name()}, ${cheese.name()}`);
  }
}

const pizza = new CheesePizza(new NYPizzaIngredientFactory());
pizza.prepare();

Consequences

Applying the Abstract Factory pattern results in several significant architectural trade-offs. The original GoF catalog identifies four:

  • It isolates concrete classes. The factory encapsulates the responsibility and the process of creating product objects, so clients manipulate instances only through their abstract interfaces. Concrete product class names are isolated inside the concrete factory and never appear in client code.
  • It makes exchanging product families easy. Because the concrete factory class appears only once in an application (where it’s instantiated), swapping the entire product family is a one-line change—switch the factory, and the whole family changes at once. In the GoF widget-toolkit example, you switch from Motif to Presentation Manager simply by swapping MotifWidgetFactory for PMWidgetFactory. In the pizza example, you switch a franchise’s region by passing a different PizzaIngredientFactory.
  • It promotes consistency among products. When products in a family are designed to work together, the pattern enforces that an application uses objects from only one family at a time, preventing incompatible combinations (e.g., NY thin-crust dough with Chicago plum-tomato sauce).
  • Supporting new kinds of products is difficult. While adding new families is easy (write a new concrete factory + product implementations), adding new types of products is hard. Adding “Pepperoni” to the ingredient family requires changing the PizzaIngredientFactory interface and modifying every concrete factory subclass to implement the new method. This is a fundamental asymmetry: the pattern makes one axis of change easy (new families) at the cost of making the other axis hard (new product types).

Implementation Notes

The original GoF catalog highlights three useful techniques for implementing Abstract Factory:

  • Factories as Singletons. An application typically needs only one instance of a ConcreteFactory per product family, so the concrete factory is often implemented as a Singleton. One NYPizzaIngredientFactory and one ChicagoPizzaIngredientFactory is usually all you need.
  • Creating products with Factory Methods. AbstractFactory only declares an interface for creating products; it’s up to ConcreteFactory subclasses to actually create them. The most common implementation is to define a Factory Method for each product, and have each concrete factory override those methods. (This is exactly the shape of the example above: each createX() slot is itself a Factory Method.) An alternative—useful when many product families exist—is to use the Prototype pattern: the concrete factory stores a prototypical instance of each product and creates new ones by cloning.
  • Defining extensible factories. Because AbstractFactory typically defines a separate operation per product kind, adding a new kind of product means changing the interface and every subclass. A more flexible (but less type-safe) variation collapses all the per-product operations into a single parameterized make(kind) operation, where the parameter identifies the kind of product to create. This trades compile-time type checking for the ability to add new product kinds without touching the interface.

Known Uses

The pattern shows up across very different domains:

  • GUI widget toolkits. GoF’s motivating example: a WidgetFactory interface with concrete MotifWidgetFactory and PMWidgetFactory (Presentation Manager) subclasses, each producing a coordinated family of windows, scroll bars, and buttons for one look-and-feel.
  • InterViews Kit classes. InterViews uses the Kit suffix to mark Abstract Factory classes—WidgetKit and DialogKit produce look-and-feel-specific UI objects, and LayoutKit produces composition objects appropriate to a desired layout (e.g., portrait vs. landscape).
  • ET++ window-system portability. ET++ uses Abstract Factory to achieve portability across window systems (X Windows, SunView). A WindowSystem abstract base class declares operations like MakeWindow, MakeFont, and MakeColor; each concrete subclass implements them for one specific window system.
  • Cross-region product franchises. Head First’s Pizza Store example—the basis for the running example on this page—uses a PizzaIngredientFactory to ship region-appropriate dough, sauce, cheese, veggies, pepperoni, and clams to each franchise.

Related Patterns

  • Factory Method. AbstractFactory operations are most commonly implemented with Factory Methods—each createX() slot is itself a Factory Method that a concrete factory subclass overrides.
  • Prototype. An alternative implementation of Abstract Factory: instead of subclassing for each product family, the concrete factory holds a prototypical instance of each product and creates new ones by cloning.
  • Singleton. A concrete factory is often a Singleton, since one instance per product family typically suffices.

Comparing the Creational Patterns

Understanding when each creational pattern applies requires examining which sub-problem of object creation each one solves:

Comparison point Factory Method Abstract Factory Builder
Focus One product type Family of related product types Complex product with many parts
Mechanism Inheritance (subclass overrides) Composition (client receives factory object) Step-by-step construction algorithm
Adding new variants Add new Creator subclass Add new Concrete Factory + products Add new Builder subclass
Adding new product types N/A (only one product) Difficult (change interface + all factories) Add new build step
Complexity Low High (most variation points) Medium
Key benefit Simplicity Enforces family consistency Communicates product structure

A common framing captures the relationship: Factory Method relies on inheritance—you extend a creator and override the factory method. Abstract Factory relies on object composition—you pass a factory object to the client, and the factory creates the products. (In practice, the two patterns are often layered: each createX() slot inside an Abstract Factory is itself a Factory Method.)

Flashcards

Factory Method & Abstract Factory Flashcards

Key concepts and comparisons for creational design patterns.

Difficulty: Intermediate

What problem does Factory Method solve?

Difficulty: Basic

What are the four roles in Factory Method?

Difficulty: Intermediate

Factory Method vs. Abstract Factory: when to use which?

Difficulty: Advanced

What is a parameterized factory method?

Difficulty: Advanced

How does Factory Method relate to Abstract Factory?

Difficulty: Advanced

What is the ‘Rigid Interface’ drawback of Abstract Factory?

Difficulty: Intermediate

Abstract Factory uses __ ; Factory Method uses __.

Quiz

Factory Method & Abstract Factory Quiz

Test your understanding of creational patterns — when to use which, design decisions, and their relationships.

Difficulty: Intermediate

A PizzaStore uses a parameterized factory method: createPizza(String type) with an if/else chain to decide which pizza to create. A new pizza type (“BBQ Chicken”) must be added. What is the design problem?

Correct Answer:
Difficulty: Intermediate

A system needs to create families of related UI components (Button, TextField, Checkbox) that must be visually consistent — all from the same theme (Material, iOS, Windows). Which pattern is most appropriate?

Correct Answer:
Difficulty: Advanced

A common shorthand contrasts Factory Method and Abstract Factory along an inheritance-vs-composition axis. What does that contrast mean structurally?

Correct Answer:
Difficulty: Advanced

An Abstract Factory interface has 12 creation methods (one per product type). A new product type must be added. What is the consequence?

Correct Answer:
Difficulty: Advanced

Each method in a PizzaIngredientFactorycreateDough(), createSauce(), createCheese() — is implemented differently by NYPizzaIngredientFactory and ChicagoPizzaIngredientFactory. What is the relationship between these creation methods and the Factory Method pattern?

Correct Answer:

Adapter


Context

In software construction, we frequently encounter situations where an existing system needs to collaborate with a third-party library, a vendor class, or legacy code. However, these external components often have interfaces that do not match the specific “Target” interface our system was designed to use.

A classic real-world analogy is the power outlet adapter. If you take a US laptop to London, the laptop’s plug (the client) expects a US power interface, but the wall outlet (the adaptee) provides a European interface. To make them work together, you need an adapter that translates the interface of the wall outlet into one the laptop can plug into. In software, the Adapter pattern acts as this “middleman”, allowing classes to work together that otherwise couldn’t due to incompatible interfaces.

Problem

The primary challenge occurs when we want to use an existing class, but its interface does not match the one we need. This typically happens for several reasons:

  • Legacy Code: We have code written a long time ago that we don’t want to (or can’t) change, but it must fit into a new, more modern architecture.
  • Vendor Lock-in: We are using a vendor class that we cannot modify, yet its method names or parameters don’t align with our system’s requirements.
  • Syntactic and Semantic Mismatches: Two interfaces might differ in syntax (e.g., getDistance() in inches vs. getLength() in meters) or semantics (e.g., a method that performs a similar action but with different side effects).

Without an adapter, we would be forced to rewrite our existing system code to accommodate every new vendor or legacy class, which violates the Open/Closed Principle and creates tight coupling.

Solution

The Adapter Pattern solves this by creating a class that converts the interface of an “Adaptee” class into the “Target” interface that the “Client” expects.

According to the GoF catalog, there are four key roles in this structure:

  1. Target: The domain-specific interface the Client wants to use (e.g., a Duck interface with quack() and fly()). In GoF’s motivating example, this is Shape.
  2. Adaptee: The existing class with an incompatible interface that needs adapting (e.g., a WildTurkey class that gobble()s instead of quack()s). In GoF, this is TextView.
  3. Adapter: The class that adapts the interface of Adaptee to the Target interface (e.g., TurkeyAdapter). In GoF, this is TextShape.
  4. Client: The class that collaborates with objects conforming to the Target interface, remaining oblivious to the fact that it is communicating with an Adaptee through the Adapter.

In the “Turkey that wants to be a Duck” example, we create a TurkeyAdapter that implements the Duck interface. When the client calls quack() on the adapter, the adapter internally calls gobble() on the wrapped turkey object. Because turkeys can only fly short distances, the adapter calls the turkey’s fly() method five times to compensate when a duck-style fly() is requested. This syntactic translation effectively hides the underlying implementation from the client.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

This example adapts a Turkey so client code that expects a Duck can keep using the same target interface.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface Duck {
    void quack();
    void fly();
}

interface Turkey {
    void gobble();
    void fly();
}

final class WildTurkey implements Turkey {
    public void gobble() {
        System.out.println("Gobble gobble");
    }

    public void fly() {
        System.out.println("I'm flying a short distance");
    }
}

final class TurkeyAdapter implements Duck {
    private final Turkey turkey;

    TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }

    public void quack() {
        turkey.gobble();
    }

    public void fly() {
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }
}

public class Demo {
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }

    public static void main(String[] args) {
        testDuck(new TurkeyAdapter(new WildTurkey()));
    }
}
#include <iostream>

struct Duck {
    virtual ~Duck() = default;
    virtual void quack() = 0;
    virtual void fly() = 0;
};

struct Turkey {
    virtual ~Turkey() = default;
    virtual void gobble() = 0;
    virtual void fly() = 0;
};

class WildTurkey : public Turkey {
public:
    void gobble() override {
        std::cout << "Gobble gobble\n";
    }

    void fly() override {
        std::cout << "I'm flying a short distance\n";
    }
};

class TurkeyAdapter : public Duck {
public:
    explicit TurkeyAdapter(Turkey& turkey) : turkey_(turkey) {}

    void quack() override {
        turkey_.gobble();
    }

    void fly() override {
        for (int i = 0; i < 5; ++i) {
            turkey_.fly();
        }
    }

private:
    Turkey& turkey_;
};

void testDuck(Duck& duck) {
    duck.quack();
    duck.fly();
}

int main() {
    WildTurkey turkey;
    TurkeyAdapter adapter(turkey);
    testDuck(adapter);
}
from abc import ABC, abstractmethod


class Duck(ABC):
    @abstractmethod
    def quack(self) -> None:
        pass

    @abstractmethod
    def fly(self) -> None:
        pass


class Turkey(ABC):
    @abstractmethod
    def gobble(self) -> None:
        pass

    @abstractmethod
    def fly(self) -> None:
        pass


class WildTurkey(Turkey):
    def gobble(self) -> None:
        print("Gobble gobble")

    def fly(self) -> None:
        print("I'm flying a short distance")


class TurkeyAdapter(Duck):
    def __init__(self, turkey: Turkey) -> None:
        self._turkey = turkey

    def quack(self) -> None:
        self._turkey.gobble()

    def fly(self) -> None:
        for _ in range(5):
            self._turkey.fly()


def test_duck(duck: Duck) -> None:
    duck.quack()
    duck.fly()


test_duck(TurkeyAdapter(WildTurkey()))
interface Duck {
  quack(): void;
  fly(): void;
}

interface Turkey {
  gobble(): void;
  fly(): void;
}

class WildTurkey implements Turkey {
  gobble(): void {
    console.log("Gobble gobble");
  }

  fly(): void {
    console.log("I'm flying a short distance");
  }
}

class TurkeyAdapter implements Duck {
  constructor(private readonly turkey: Turkey) {}

  quack(): void {
    this.turkey.gobble();
  }

  fly(): void {
    for (let i = 0; i < 5; i += 1) {
      this.turkey.fly();
    }
  }
}

function testDuck(duck: Duck): void {
  duck.quack();
  duck.fly();
}

testDuck(new TurkeyAdapter(new WildTurkey()));

Consequences

Applying the Adapter pattern results in several significant architectural trade-offs:

  • Loose Coupling: It decouples the client from the legacy or vendor code. The client only knows the Target interface, allowing the Adaptee to evolve independently without breaking the client code.
  • Information Hiding: It follows the Information Hiding principle by concealing the “secret” that the system is using a legacy component.
  • Flexibility vs. Complexity: While adapters make a system more flexible, they add a layer of indirection that can make it harder to trace the execution flow of the program since the client doesn’t know which object is actually receiving the call.

Design Decisions

Object Adapter vs. Class Adapter

  • Object Adapter (via composition): The adapter wraps an instance of the Adaptee. This is the standard approach in Java and most modern languages. It can adapt an entire class hierarchy (any subclass of the Adaptee works), and the adaptation can be configured at runtime.
  • Class Adapter (via inheritance): The adapter inherits from both the Target and the Adaptee simultaneously. This requires either multiple class inheritance (e.g., C++) or — in single-inheritance languages — the Target to be an interface, so the adapter can extend Adaptee and implements Target. It avoids the indirection overhead of delegation but ties the adapter to a single concrete Adaptee class.

Modern practice favors Object Adapters because they compose with any subclass of the Adaptee, can be reconfigured at runtime, and don’t require either party to be open for inheritance (see also Effective Java Item 18: Favor composition over inheritance).

Adaptation Scope

Not all adapters are created equal. The complexity of adaptation ranges widely:

  • Simple rename: quack() maps directly to gobble(). Trivial and low-risk.
  • Data transformation: Converting units, reformatting data structures, or translating between protocols. Moderate complexity.
  • Behavioral adaptation: The adaptee’s behavior is fundamentally different and the adapter must add logic to bridge the semantic gap. High complexity—and a warning sign that the adapter may be growing into a service.

If an adapter becomes “too thick” (containing significant business logic), it is no longer just translating an interface—it has become a separate component that happens to look like an adapter.

Adapter is a Family, Not a Single Pattern

Buschmann, Henney, and Schmidt observe in Pattern-Oriented Software Architecture, Volume 5: On Patterns and Pattern Languages (2007, p. 234) that “the notion that there is a single pattern called Adapter is in practice present nowhere except in the table of contents of the Gang-of-Four book.” A deconstruction of GoF’s pattern description reveals at least four quite distinct patterns:

  1. Object Adapter: Wraps an adaptee via composition; adaptation is encapsulated through forwarding via an additional level of indirection (the standard form, favored from a layered/encapsulated perspective).
  2. Class Adapter: Realized by subclassing both the adapter interface (Target) and the adaptee implementation to yield a single object — avoiding an additional level of indirection. Requires multiple inheritance, or — in single-inheritance languages — the Target being an interface.
  3. Two-Way Adapter: Conforms to both the target and adaptee interfaces (typically via multiple inheritance), so the adapter is usable wherever either interface is expected. GoF’s example is ConstraintStateVariable, a subclass of both Unidraw’s StateVariable and QOCA’s ConstraintVariable, that adapts each interface to the other so the same object works in either system.
  4. Pluggable Adapter: A class with built-in interface adaptation. GoF describes three implementations: using abstract operations, using delegate objects, or using parameterized adapters (e.g., Smalltalk’s PluggableAdaptor, which is parameterized with blocks).

The first two forms (Object Adapter, Class Adapter) are described together inside GoF’s Adapter entry, while Two-Way and Pluggable Adapter are surfaced in GoF’s Implementation discussion. This insight is educationally important: when a reference says “use the Adapter pattern”, you must clarify which form of adaptation is needed.

Adapter vs. Facade vs. Decorator

These three patterns all “wrap” another object, but with different intents:

Pattern Intent Scope
Adapter Convert one interface to match another One-to-one: translates a single incompatible interface
Façade Simplify a complex set of interfaces Many-to-one: wraps an entire subsystem behind one interface
Decorator Add behavior to an object without changing its interface One-to-one: wraps a single object, preserving its interface

The key discriminator: Adapter changes what the interface looks like. Facade changes how much of the interface you see. Decorator changes what the object does through the same interface.

Flashcards

Structural Pattern Flashcards

Key concepts for Adapter, Composite, and Facade patterns.

Difficulty: Basic

What problem does Adapter solve?

Difficulty: Intermediate

Object Adapter vs. Class Adapter?

Difficulty: Intermediate

Adapter vs. Facade vs. Decorator?

Difficulty: Advanced

What does POSA5 say about ‘the Adapter pattern’?

Difficulty: Basic

What problem does Composite solve?

Difficulty: Advanced

Composite: Transparent vs. Safe design?

Difficulty: Advanced

Name three pattern compounds involving Composite.

Difficulty: Basic

What problem does Facade solve?

Difficulty: Advanced

Facade vs. Mediator: what’s the communication direction?

Difficulty: Intermediate

Should the subsystem know about its Facade?

Quiz

Structural Patterns Quiz

Test your understanding of Adapter, Composite, and Facade — their distinctions, design decisions, and when to apply each.

Difficulty: Advanced

A TurkeyAdapter implements the Duck interface. The fly() method calls turkey.fly() five times in a loop because a duck’s flight is much longer than a turkey’s short hop. What design concern does this raise?

Correct Answer:
Difficulty: Intermediate

A colleague says: “We should use an Adapter between our service and the database layer.” Your team wrote both the service and the database layer. What is the best response?

Correct Answer:
Difficulty: Intermediate

In a Composite pattern for a restaurant menu system, a developer declares add(MenuComponent) on the abstract MenuComponent class (inherited by both Menu and MenuItem). A tester calls menuItem.add(anotherItem). What happens, and what design trade-off does this illustrate?

Correct Answer:
Difficulty: Advanced

All three patterns — Adapter, Facade, and Decorator — involve “wrapping” another object. What is the key distinction between them?

Correct Answer:
Difficulty: Advanced

A HomeTheaterFacade exposes watchMovie(), endMovie(), listenToMusic(), stopMusic(), playGame(), setupKaraoke(), and calibrateSystem(). The class is growing difficult to maintain. What is the best architectural response?

Correct Answer:
Difficulty: Advanced

The Facade’s communication is one-directional: the Facade calls subsystem classes, but the subsystem does not know about the Facade. The Mediator’s communication is bidirectional. Why does this distinction matter architecturally?

Correct Answer:

Singleton


Context

In software engineering, certain classes represent concepts that should only exist once during the entire execution of a program. The original GoF motivating examples capture this well: a system may have many printers but only one printer spooler, only one file system, and only one window manager. Modern variations include thread pools, caches, dialog boxes, logging objects, and device drivers. In these scenarios, having more than one instance is not just unnecessary but often harmful to the system’s integrity. In a UML class diagram, this requirement is explicitly modeled by specifying a multiplicity of “1” in the upper right corner of the class box, indicating the class is intended to be a singleton.

Problem

The primary problem arises when instantiating more than one of these unique objects leads to incorrect program behavior, resource overuse, or inconsistent results. For instance, accidentally creating two distinct “Earth” objects in a planetary simulation would break the logic of the system.

While developers might be tempted to use global variables to manage these unique objects, this approach introduces several critical flaws:

  • High Coupling: Global variables allow any part of the system to access and potentially mess around with the object, creating a web of dependencies that makes the code hard to maintain.
  • Lack of Control: Global variables do not prevent a developer from accidentally calling the constructor multiple times to create a second, distinct instance.
  • Instantiation Issues: You may want the flexibility to choose between “eager instantiation” (creating the object at program start) or “lazy instantiation” (creating it only when first requested), which simple global variables do not inherently support.

Solution

The Singleton Pattern solves these issues by ensuring a class has only one instance while providing a controlled, global point of access to it. The solution consists of three main implementation aspects:

  1. A Private Constructor: By declaring the constructor private, the pattern prevents external classes from ever using the new keyword to create an instance.
  2. A Static Field: The class maintains a private static variable (often named uniqueInstance) to hold its own single instance.
  3. A Static Access Method: A public static method, typically named getInstance(), serves as the sole gateway to the object.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

This example models a process-wide configuration/logger object. Each language has a different idiom for enforcing one instance; the intent is the same: clients do not call the constructor directly.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

public final class AppConfig {
    private static final AppConfig INSTANCE = new AppConfig();

    private AppConfig() {}

    public static AppConfig getInstance() {
        return INSTANCE;
    }

    public void log(String message) {
        System.out.println("[config] " + message);
    }
}

public class Demo {
    public static void main(String[] args) {
        AppConfig first = AppConfig.getInstance();
        AppConfig second = AppConfig.getInstance();
        first.log("same instance: " + (first == second));
    }
}
#include <iostream>
#include <string>

class AppConfig {
public:
    static AppConfig& instance() {
        static AppConfig config;
        return config;
    }

    AppConfig(const AppConfig&) = delete;
    AppConfig& operator=(const AppConfig&) = delete;

    void log(const std::string& message) const {
        std::cout << "[config] " << message << "\n";
    }

private:
    AppConfig() = default;
};

int main() {
    AppConfig& first = AppConfig::instance();
    AppConfig& second = AppConfig::instance();
    first.log(&first == &second ? "same instance" : "different instances");
}
from __future__ import annotations


class AppConfig:
    _instance: AppConfig | None = None

    def __new__(cls) -> AppConfig:
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def log(self, message: str) -> None:
        print(f"[config] {message}")


first = AppConfig()
second = AppConfig()
first.log(f"same instance: {first is second}")

Pythonic alternative. The __new__ form has a well-known pitfall: Python still calls __init__ on every AppConfig() call, so if the class ever grows an __init__, it will silently re-initialize state. The standard Pythonic singleton is just a module-level instance — modules are loaded once and cached, so a top-level config = AppConfig() in config.py is already a singleton, with no metaclass or __new__ trickery.

class AppConfig {
  private static instance: AppConfig | undefined;

  private constructor() {}

  static getInstance(): AppConfig {
    AppConfig.instance ??= new AppConfig();
    return AppConfig.instance;
  }

  log(message: string): void {
    console.log(`[config] ${message}`);
  }
}

const first = AppConfig.getInstance();
const second = AppConfig.getInstance();
first.log(`same instance: ${first === second}`);

Refining the Solution: Thread Safety and Performance

The Java example above uses eager instantiation: the instance is created when the class is first loaded. The JVM guarantees class initialization runs exactly once, so this is automatically thread-safe. The trade-off is that the object is built even if no client ever calls getInstance().

A common alternative is lazy instantiation, which only creates the instance on the first call:

// NOT thread-safe — for illustration only
public static AppConfig getInstance() {
    if (instance == null) {            // (1) check
        instance = new AppConfig();    // (2) create
    }
    return instance;
}

This naive form is not thread-safe: if two threads run (1) simultaneously and both see null, they will both run (2) and create two separate objects. Java offers several ways to fix this:

  • Synchronized Method: Adding the synchronized keyword to getInstance() makes the check-and-create atomic, but introduces lock-acquisition overhead on every call, even after the object has been created.
  • Eager Instantiation: As shown above. Simple, thread-safe, no synchronization — at the cost of building the object up front.
  • Double-Checked Locking (DCL): Check for null before entering a synchronized block and again inside it, so the lock is taken only on the first call. This idiom was famously broken before Java 5: without volatile, the JIT can reorder the constructor’s writes with the publish of the reference, so another thread can observe the field as non-null while the object is still partially constructed. From Java 5 onward, declaring the instance field volatile adds the memory barriers needed to make DCL correct. The pattern is fiddly enough that the next two idioms are usually preferred.
  • Initialization-on-Demand Holder Idiom (Bill Pugh): Put the instance in a private static nested class. The JVM only loads the holder class when it is first referenced (lazy), and class initialization is guaranteed thread-safe (no volatile, no synchronized needed). This is the recommended lazy pattern in Java.
public final class AppConfig {
    private AppConfig() {}
    private static class Holder {
        static final AppConfig INSTANCE = new AppConfig();
    }
    public static AppConfig getInstance() {
        return Holder.INSTANCE;
    }
}
  • Enum Singleton: Joshua Bloch (Effective Java, Item 3) recommends a single-element enum as the most robust singleton in Java: it is concise, thread-safe by construction, and — uniquely — defends against both serialization (deserialization will not produce a second instance) and reflection attacks (the JVM forbids reflective creation of enum values).
public enum AppConfig {
    INSTANCE;
    public void log(String message) {
        System.out.println("[config] " + message);
    }
}

Other languages. The table is largely a Java-specific concern. In C++, the function-local static “Meyers’ Singleton” shown above is thread-safe by the language standard since C++11. In Python, the most idiomatic singleton is a module-level instance — modules are themselves loaded once and cached, so a top-level config = AppConfig() in config.py is already a singleton, with none of the __new__ / __init__ pitfalls of the class-based form.

Consequences

Applying the Singleton Pattern results in several important architectural outcomes:

  • Controlled Access: The pattern provides a single point of access that can be easily managed and updated.
  • Resource Efficiency: It prevents the system from being cluttered with redundant, resource-intensive objects.
  • The Risk of “Singleitis”: A major drawback is the tendency for developers to overuse the pattern. Using a Singleton just for easy global access can lead to a hard-to-maintain design with high coupling, where it becomes unclear which classes depend on the Singleton and why.
  • Complexity in Testing: Singletons are hard to mock during unit testing because they maintain state throughout the lifespan of the application. A static getInstance() call is a hardcoded dependency — there is no seam where a test double can be injected, and tests that share the singleton interfere with each other through its retained state. This is one of the main reasons many practitioners — particularly those who practise test-driven development — treat the pattern as an anti-pattern.
  • Single Responsibility Principle Violation: A Singleton class takes on two responsibilities: doing its real work and managing its own lifecycle (enforcing single-instance, controlling creation). These are independent concerns and ideally belong in different places.

A Pattern with a “Weak Solution”

The Singleton is perhaps the most controversial of all GoF patterns. Buschmann et al. (POSA5) describe it as “a well-known pattern with a weak solution”, noting that “the literature that discusses [Singleton’s] issues dwarfs the page count of the original pattern description in the Gang-of-Four book.” The core problem is that the pattern conflates two separate concerns:

  1. Ensuring a single instance—a legitimate design constraint.
  2. Providing global access—a convenience that introduces hidden coupling.

Modern practice separates these concerns. A dependency injection (DI) container can manage the singleton lifetime (ensuring only one instance exists) while keeping constructors injectable and dependencies explicit. This gives you the same lifecycle guarantee without the testability and coupling problems.

When Singleton is Acceptable

The Singleton pattern remains acceptable when:

  • It controls a true infrastructure resource that must be unique (e.g., a hardware driver in an embedded system, the JVM’s Runtime).
  • DI is genuinely unavailable (small scripts, legacy code, plug-ins loaded into a host that doesn’t expose a container).
  • The instance is immutable or otherwise stateless — a read-only configuration loaded at startup, for example, raises none of the test-isolation concerns.

In all other cases, prefer DI with singleton scope. As the maxim goes — “if your code isn’t testable, it isn’t a good design” — and a hardcoded global access point is a direct obstacle to testability.

When Singleton is an Anti-Pattern

  • When the “only one” assumption is actually a convenience assumption, not a hard requirement. Many “singletons” later need multiple instances (per-tenant, per-thread, per-test).
  • When it is used to create global state—making it impossible to reason about what depends on what.
  • When it blocks unit testing by making dependencies invisible and unmockable.

Related Patterns

The original GoF chapter notes that “many patterns can be implemented using the Singleton pattern” — typically because the pattern needs a single, well-known coordinating object:

  • Abstract Factory, Builder, and Prototype are explicitly cited by GoF as patterns that are often realised as singletons, since an application usually only needs one factory / builder / prototype registry.
  • Facade objects, by extension, are frequently singletons — there is usually one front door per subsystem.
  • Dependency Injection containers are the modern alternative discussed above: they manage singleton lifetime (one instance per scope) without the global access point, so DI subsumes most legitimate uses of the Singleton pattern.

Flashcards

Singleton Pattern Flashcards

Key concepts, controversies, and modern alternatives for the Singleton design pattern.

Difficulty: Basic

What are the three implementation aspects of Singleton?

Difficulty: Advanced

Why is Singleton controversial in modern practice?

Difficulty: Advanced

What is ‘Singleitis’?

Difficulty: Intermediate

When is Singleton acceptable in modern code?

Quiz

Singleton Pattern Quiz

Test your understanding of the Singleton pattern's controversies, thread-safety mechanisms, and modern alternatives.

Difficulty: Advanced

POSA5 describes the Singleton as “a well-known pattern with a weak solution.” What is the core reason for this criticism?

Correct Answer:
Difficulty: Intermediate

A system uses Singleton for a database connection pool. A new requirement: the system must support multi-tenant deployments with one pool per tenant. What is the fundamental problem?

Correct Answer:
Difficulty: Intermediate

A developer argues: “Our Logger class uses the Singleton pattern, and it’s fine — we never need to test it.” What is wrong with this reasoning?

Correct Answer:
Difficulty: Advanced

Which of the following are legitimate reasons to use the Singleton pattern? (Select all that apply)

Correct Answers:

Mediator


Context

In complex software systems, we often encounter a “family” of objects that must work together to achieve a high-level goal. A classic scenario is Bob’s Java-enabled smart home. In this system, various appliances like an alarm clock, a coffee maker, a calendar, and a garden sprinkler must coordinate their behaviors. For instance, when the alarm goes off, the coffee maker should start brewing, but only if it is a weekday according to the calendar.

The original GoF motivating example is a different domain: a font dialog box where widgets (a list box of font families, an entry field for the font name, and OK/Cancel buttons) must coordinate. Selecting a font in the list box updates the entry field; certain buttons enable only when text is present. The same pattern applies — the smart home is just a more relatable framing of the same underlying coordination problem.

Problem

When these objects communicate directly, several architectural challenges arise:

  • Many-to-Many Complexity: As the number of objects grows, the number of direct inter-communications grows quadratically (O(N²)), leading to a tangled web of dependencies.
  • Low Reusability: Because the coffee pot must “know” about the alarm clock and the calendar to function within Bob’s specific rules, it becomes impossible to reuse that coffee pot code in a different home that lacks a sprinkler or a specialized calendar.
  • Scattered Logic: The “rules” of the system (e.g., “no coffee on weekends”) are spread across multiple classes, making it difficult to find where to make changes when those rules evolve.
  • Inappropriate Intimacy: Objects spend too much time delving into each other’s private data or specific method names just to coordinate a simple task.

Solution

The Mediator Pattern solves this by encapsulating many-to-many communication dependencies within a single “Mediator” object. Instead of objects talking to each other directly, they only communicate with the Mediator.

The objects (often called “colleagues”) tell the Mediator when their state changes. The Mediator then contains all the complex control logic and coordination rules to tell the other objects how to respond. For example, the alarm clock simply tells the Mediator “I’ve been snoozed”, and the Mediator checks the calendar and decides whether to trigger the coffee maker. This reduces the number of inter-object connections from O(N²) to O(N), since each colleague only needs to know about the Mediator.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

This example keeps the smart-home devices reusable. The alarm, calendar, coffee maker, and sprinkler do not call each other directly; the hub owns the coordination rule.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

interface SmartHomeMediator {
    void notify(Object sender, String event);
}

final class Calendar {
    boolean isWeekday() {
        return true;
    }
}

final class CoffeeMaker {
    void brew() {
        System.out.println("Brewing coffee");
    }
}

final class Sprinkler {
    void skipMorningWatering() {
        System.out.println("Skipping sprinklers");
    }
}

final class AlarmClock {
    private final SmartHomeMediator mediator;

    AlarmClock(SmartHomeMediator mediator) {
        this.mediator = mediator;
    }

    void ring() {
        mediator.notify(this, "alarmRang");
    }
}

final class SmartHomeHub implements SmartHomeMediator {
    private final Calendar calendar = new Calendar();
    private final CoffeeMaker coffeeMaker = new CoffeeMaker();
    private final Sprinkler sprinkler = new Sprinkler();

    public void notify(Object sender, String event) {
        if ("alarmRang".equals(event) && calendar.isWeekday()) {
            coffeeMaker.brew();
            sprinkler.skipMorningWatering();
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        SmartHomeHub hub = new SmartHomeHub();
        AlarmClock alarm = new AlarmClock(hub);
        alarm.ring();
    }
}
#include <iostream>
#include <string>

struct SmartHomeMediator {
    virtual ~SmartHomeMediator() = default;
    virtual void notify(void* sender, const std::string& event) = 0;
};

class Calendar {
public:
    bool isWeekday() const { return true; }
};

class CoffeeMaker {
public:
    void brew() const { std::cout << "Brewing coffee\n"; }
};

class Sprinkler {
public:
    void skipMorningWatering() const { std::cout << "Skipping sprinklers\n"; }
};

class AlarmClock {
public:
    explicit AlarmClock(SmartHomeMediator& mediator) : mediator_(mediator) {}

    void ring() {
        mediator_.notify(this, "alarmRang");
    }

private:
    SmartHomeMediator& mediator_;
};

class SmartHomeHub : public SmartHomeMediator {
public:
    void notify(void*, const std::string& event) override {
        if (event == "alarmRang" && calendar_.isWeekday()) {
            coffeeMaker_.brew();
            sprinkler_.skipMorningWatering();
        }
    }

private:
    Calendar calendar_;
    CoffeeMaker coffeeMaker_;
    Sprinkler sprinkler_;
};

int main() {
    SmartHomeHub hub;
    AlarmClock alarm(hub);
    alarm.ring();
}
from abc import ABC, abstractmethod


class SmartHomeMediator(ABC):
    @abstractmethod
    def notify(self, sender: object, event: str) -> None:
        pass


class Calendar:
    def is_weekday(self) -> bool:
        return True


class CoffeeMaker:
    def brew(self) -> None:
        print("Brewing coffee")


class Sprinkler:
    def skip_morning_watering(self) -> None:
        print("Skipping sprinklers")


class AlarmClock:
    def __init__(self, mediator: SmartHomeMediator) -> None:
        self._mediator = mediator

    def ring(self) -> None:
        self._mediator.notify(self, "alarmRang")


class SmartHomeHub(SmartHomeMediator):
    def __init__(self) -> None:
        self.calendar = Calendar()
        self.coffee_maker = CoffeeMaker()
        self.sprinkler = Sprinkler()

    def notify(self, sender: object, event: str) -> None:
        if event == "alarmRang" and self.calendar.is_weekday():
            self.coffee_maker.brew()
            self.sprinkler.skip_morning_watering()


hub = SmartHomeHub()
alarm = AlarmClock(hub)
alarm.ring()
enum SmartHomeEvent {
  AlarmRang = "alarmRang",
}

interface SmartHomeMediator {
  notify(sender: object, event: SmartHomeEvent): void;
}

class Calendar {
  isWeekday(): boolean { return true; }
}

class CoffeeMaker {
  brew(): void { console.log("Brewing coffee"); }
}

class Sprinkler {
  skipMorningWatering(): void { console.log("Skipping sprinklers"); }
}

class AlarmClock {
  constructor(private readonly mediator: SmartHomeMediator) {}

  ring(): void {
    this.mediator.notify(this, SmartHomeEvent.AlarmRang);
  }
}

class SmartHomeHub implements SmartHomeMediator {
  private readonly calendar = new Calendar();
  private readonly coffeeMaker = new CoffeeMaker();
  private readonly sprinkler = new Sprinkler();

  notify(sender: object, event: SmartHomeEvent): void {
    if (event === SmartHomeEvent.AlarmRang && this.calendar.isWeekday()) {
      this.coffeeMaker.brew();
      this.sprinkler.skipMorningWatering();
    }
  }
}

const hub = new SmartHomeHub();
const alarm = new AlarmClock(hub);
alarm.ring();

Consequences

The GoF lists five consequences of the Mediator pattern; the first four are benefits and the fifth is the central trade-off:

  • It limits subclassing. A mediator localizes behavior that would otherwise be distributed among several colleague classes. Changing this behavior requires subclassing the Mediator only; Colleague classes can be reused as-is.
  • It decouples colleagues. Individual objects become more reusable because they make fewer assumptions about the existence of other objects or specific system requirements. You can vary and reuse Colleague and Mediator classes independently.
  • It simplifies object protocols. A mediator replaces many-to-many interactions with one-to-many interactions between the mediator and its colleagues. One-to-many relationships are easier to understand, maintain, and extend.
  • It abstracts how objects cooperate. Making mediation an independent concept and encapsulating it in an object lets you focus on how objects interact apart from their individual behavior. That can help clarify how objects interact in a system.
  • It centralizes control — the “God Class” risk. The Mediator pattern trades complexity of interaction for complexity in the mediator. Because a mediator encapsulates protocols, it can become more complex than any individual colleague — the Mediator does not actually remove the inherent complexity of the interactions; it just provides a structure for centralizing it. This can make the mediator itself a monolith that is hard to maintain.

Beyond GoF, one engineering concern is worth flagging in production systems:

  • Single point of failure / performance bottleneck. Because all communication flows through one object, a global mediator can become a reliability and performance hot spot. (This is an engineering observation, not a GoF consequence.)

Observer vs. Mediator: Distributed vs. Centralized

These two behavioral patterns are frequently confused because both deal with communication between objects. The key distinction is where the coordination logic lives:

Aspect Observer Mediator
Communication One-to-many: subject broadcasts, observers decide how to react Many-to-many: colleagues report events, mediator decides what to do
Intelligence Distributed: each observer contains its own reaction logic Centralized: the mediator contains all coordination logic
Coupling Subject knows only the Observer interface; observers are independent of each other Colleagues know only the Mediator interface; all rules live in one place
Best for Extensibility: adding new types of observers without changing the subject Changeability: modifying coordination rules without touching the colleagues
Risk Notification storms; cascading updates; hard-to-predict interaction order God class; single point of failure; complexity displacement

A useful heuristic: if the objects need to react independently to a change (each observer does its own thing), use Observer. If the objects need to be coordinated (the response depends on the collective state of multiple objects), use Mediator.

In practice, the two patterns are often combined: colleagues use Observer-style notifications to inform the mediator, and the mediator uses direct method calls to coordinate the response. This composition gives you the loose coupling of Observer with the centralized coordination of Mediator. The GoF Related Patterns section explicitly notes: “Colleagues can communicate with the mediator using the Observer pattern.” GoF also describes the ChangeManager from the Observer chapter as a Mediator instance — the same idea seen from the other direction.

Façade vs. Mediator: External Simplification vs. Internal Coordination

Mediator is also frequently confused with Façade, because both put a single object in front of a group of others. The distinction is about direction and awareness:

Aspect Façade Mediator
Direction One-way: external clients call into the façade, which forwards to the subsystem. The subsystem objects do not know the façade exists. Multi-way: colleagues call into the mediator, and the mediator calls back into colleagues. Both sides know each other.
Goal Hide the complexity of a subsystem behind a simpler interface for outside use. Coordinate the interactions among a set of peer objects so they don’t have to know each other.
Subsystem awareness Subsystem classes are unchanged and unaware of the façade. Colleague classes are explicitly designed to talk through the mediator.

If clients outside a module need a simple way in, that’s a Façade. If peers inside a module need a way to coordinate without referring to each other, that’s a Mediator.

Design Decisions

Event-Based vs. Direct Method Calls

  • Event-based: Colleagues emit named events (strings or enums), and the mediator matches events to responses. More flexible and decoupled, but harder to trace in a debugger.
  • Direct method calls: The mediator has typed methods for each coordination scenario (e.g., onAlarmRang(), onCalendarUpdated()). Easier to understand but tightly couples the mediator to the specific set of colleagues.

Scope of Mediation

  • Per-conversation mediator: A new mediator is created for each interaction session (common in chat applications or wizard-style UIs).
  • Global mediator: A single mediator manages all interactions in a subsystem (the smart home example). Simpler but increases the risk of the god class problem.

Abstract Mediator vs. Concrete-Only

GoF notes that the abstract Mediator class is sometimes optional. If colleagues only ever work with one concrete mediator, you can skip the abstract layer. The abstract class earns its keep when colleagues need to be reusable across multiple ConcreteMediator subclasses — the abstract coupling is what makes that reuse possible.

Flashcards

Mediator Pattern Flashcards

Key concepts, design decisions, and the Observer vs. Mediator comparison.

Difficulty: Intermediate

What problem does Mediator solve?

Difficulty: Intermediate

Observer vs. Mediator: key difference?

Difficulty: Intermediate

When to use Observer vs. Mediator?

Difficulty: Advanced

What is the ‘god class’ risk of Mediator?

Difficulty: Advanced

What is a ‘Managed Observer’?

Quiz

Mediator Pattern Quiz

Test your understanding of the Mediator pattern, its trade-offs, and its relationship to Observer.

Difficulty: Advanced

In a smart home, the AlarmClock, CoffeeMaker, Calendar, and Sprinkler coordinate via a SmartHomeHub (Mediator). The rule is: “When the alarm rings on a weekday, brew coffee and skip watering.” If the team used Observer instead (CoffeeMaker observes AlarmClock directly), where would the “only on weekdays” rule live?

Correct Answer:
Difficulty: Advanced

What is the core difference between Observer and Mediator?

Correct Answer:
Difficulty: Advanced

A Mediator for a complex system has grown to 2,000 lines of coordination logic. What design problem has occurred, and what is the best remedy?

Correct Answer:
Difficulty: Advanced

A “Managed Observer” is a pattern compound that combines Observer and Mediator. What emergent property does this combination provide?

Correct Answer:
Difficulty: Advanced

The Mediator pattern converts N-to-N dependencies into N-to-1 dependencies. Why doesn’t this always reduce overall system complexity?

Correct Answer:

Facade


Context

In modern software construction, we often build systems composed of multiple complex subsystems that must collaborate to perform a high-level task. A classic example used by Freeman & Robson in Head First Design Patterns is a Home Theater System consisting of various independent components: an amplifier, a tuner, a DVD player, a CD player, a projector, a motorized screen, theater lights, and a popcorn popper. The Gang of Four use a different running example — a compiler subsystem containing classes like Scanner, Parser, ProgramNode, BytecodeStream, and ProgramNodeBuilder — but the underlying problem is the same: each component is a powerful “module” on its own, but they must be coordinated precisely to provide a seamless user experience.

Problem

When a client needs to interact with a set of complex subsystems, several issues arise:

  1. High Complexity: To perform a single logical action like “Watch a Movie”, the client must execute a long sequence of manual steps. In the Head First example, watching a movie requires 13 separate calls across six classes: turn on the popcorn popper, start it popping, dim the lights, put the screen down, turn on the projector, set its input, put it in widescreen mode, turn on the amplifier, set it to DVD input, set surround sound, set the volume, turn on the DVD player, and finally play the movie.
  2. Maintenance Nightmares: If the movie finishes, the user has to perform all those steps again in reverse order to shut everything down. If a component is upgraded (e.g., replacing the DVD player with a Blu-ray device), every client that uses the system must learn a new, slightly different procedure.
  3. Tight Coupling: The client code becomes “intimate” with every single class in the subsystem. This violates the principle of Information Hiding, as the client must understand the internal low-level details of how each device operates just to use the system.

Solution

The Façade Pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use by wrapping complexity behind a single, simplified object.

In the Home Theater example, we create a HomeTheaterFaçade. Instead of the client calling twelve different methods on six different objects, the client calls one high-level method: watchMovie(). The Façade object then handles the “dirty work” of delegating those requests to the underlying subsystems. This creates a single point of use for the entire component, effectively hiding the complex “how” of the implementation from the outside world.

UML Role Diagram

UML Example Diagram

Sequence Diagram

Code Example

This example gives clients one intention-revealing operation, watchMovie(), while the facade coordinates the subsystem calls in the required order.

Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.

final class Amplifier {
    void on() { System.out.println("Amplifier on"); }
    void off() { System.out.println("Amplifier off"); }
    void setDvd(DvdPlayer dvd) { System.out.println("Amplifier setting DVD player"); }
    void setSurroundSound() { System.out.println("Amplifier surround sound on"); }
    void setVolume(int level) { System.out.println("Amplifier setting volume to " + level); }
}

final class Projector {
    void on() { System.out.println("Projector on"); }
    void off() { System.out.println("Projector off"); }
    void wideScreenMode() { System.out.println("Projector in widescreen mode"); }
}

final class TheaterLights {
    void on() { System.out.println("Lights on"); }
    void dim(int level) { System.out.println("Lights dimmed to " + level); }
}

final class Screen {
    void up() { System.out.println("Screen going up"); }
    void down() { System.out.println("Screen going down"); }
}

final class PopcornPopper {
    void on() { System.out.println("Popcorn Popper on"); }
    void off() { System.out.println("Popcorn Popper off"); }
    void pop() { System.out.println("Popcorn Popper popping popcorn!"); }
}

final class DvdPlayer {
    void on() { System.out.println("DVD Player on"); }
    void off() { System.out.println("DVD Player off"); }
    void play(String movie) { System.out.println("DVD Player playing \"" + movie + "\""); }
    void stop() { System.out.println("DVD Player stopped"); }
    void eject() { System.out.println("DVD Player eject"); }
}

final class HomeTheaterFaçade {
    private final Amplifier amp;
    private final DvdPlayer dvd;
    private final Projector projector;
    private final TheaterLights lights;
    private final Screen screen;
    private final PopcornPopper popper;

    HomeTheaterFaçade(Amplifier amp, DvdPlayer dvd, Projector projector,
                      TheaterLights lights, Screen screen, PopcornPopper popper) {
        this.amp = amp;
        this.dvd = dvd;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popper = popper;
    }

    void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        popper.on();
        popper.pop();
        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);
        dvd.on();
        dvd.play(movie);
    }

    void endMovie() {
        System.out.println("Shutting movie theater down...");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        dvd.stop();
        dvd.eject();
        dvd.off();
    }
}

public class Demo {
    public static void main(String[] args) {
        HomeTheaterFaçade homeTheater = new HomeTheaterFaçade(
            new Amplifier(), new DvdPlayer(), new Projector(),
            new TheaterLights(), new Screen(), new PopcornPopper());
        homeTheater.watchMovie("Raiders of the Lost Ark");
        homeTheater.endMovie();
    }
}
#include <iostream>
#include <string>

class DvdPlayer {
public:
    void on() const { std::cout << "DVD Player on\n"; }
    void off() const { std::cout << "DVD Player off\n"; }
    void play(const std::string& movie) const { std::cout << "DVD Player playing \"" << movie << "\"\n"; }
    void stop() const { std::cout << "DVD Player stopped\n"; }
    void eject() const { std::cout << "DVD Player eject\n"; }
};

class Amplifier {
public:
    void on() const { std::cout << "Amplifier on\n"; }
    void off() const { std::cout << "Amplifier off\n"; }
    void setDvd(const DvdPlayer&) const { std::cout << "Amplifier setting DVD player\n"; }
    void setSurroundSound() const { std::cout << "Amplifier surround sound on\n"; }
    void setVolume(int level) const { std::cout << "Amplifier setting volume to " << level << "\n"; }
};

class Projector {
public:
    void on() const { std::cout << "Projector on\n"; }
    void off() const { std::cout << "Projector off\n"; }
    void wideScreenMode() const { std::cout << "Projector in widescreen mode\n"; }
};

class TheaterLights {
public:
    void on() const { std::cout << "Lights on\n"; }
    void dim(int level) const { std::cout << "Lights dimmed to " << level << "\n"; }
};

class Screen {
public:
    void up() const { std::cout << "Screen going up\n"; }
    void down() const { std::cout << "Screen going down\n"; }
};

class PopcornPopper {
public:
    void on() const { std::cout << "Popcorn Popper on\n"; }
    void off() const { std::cout << "Popcorn Popper off\n"; }
    void pop() const { std::cout << "Popcorn Popper popping popcorn!\n"; }
};

class HomeTheaterFaçade {
public:
    HomeTheaterFaçade(Amplifier& amp, DvdPlayer& dvd, Projector& projector,
                      TheaterLights& lights, Screen& screen, PopcornPopper& popper)
        : amp_(amp), dvd_(dvd), projector_(projector),
          lights_(lights), screen_(screen), popper_(popper) {}

    void watchMovie(const std::string& movie) const {
        std::cout << "Get ready to watch a movie...\n";
        popper_.on();
        popper_.pop();
        lights_.dim(10);
        screen_.down();
        projector_.on();
        projector_.wideScreenMode();
        amp_.on();
        amp_.setDvd(dvd_);
        amp_.setSurroundSound();
        amp_.setVolume(5);
        dvd_.on();
        dvd_.play(movie);
    }

    void endMovie() const {
        std::cout << "Shutting movie theater down...\n";
        popper_.off();
        lights_.on();
        screen_.up();
        projector_.off();
        amp_.off();
        dvd_.stop();
        dvd_.eject();
        dvd_.off();
    }

private:
    Amplifier& amp_;
    DvdPlayer& dvd_;
    Projector& projector_;
    TheaterLights& lights_;
    Screen& screen_;
    PopcornPopper& popper_;
};

int main() {
    Amplifier amp;
    DvdPlayer dvd;
    Projector projector;
    TheaterLights lights;
    Screen screen;
    PopcornPopper popper;
    HomeTheaterFaçade homeTheater(amp, dvd, projector, lights, screen, popper);
    homeTheater.watchMovie("Raiders of the Lost Ark");
    homeTheater.endMovie();
}
class Amplifier:
    def on(self) -> None:
        print("Amplifier on")

    def off(self) -> None:
        print("Amplifier off")

    def set_dvd(self, dvd: "DvdPlayer") -> None:
        print("Amplifier setting DVD player")

    def set_surround_sound(self) -> None:
        print("Amplifier surround sound on")

    def set_volume(self, level: int) -> None:
        print(f"Amplifier setting volume to {level}")


class Projector:
    def on(self) -> None:
        print("Projector on")

    def off(self) -> None:
        print("Projector off")

    def wide_screen_mode(self) -> None:
        print("Projector in widescreen mode")


class TheaterLights:
    def on(self) -> None:
        print("Lights on")

    def dim(self, level: int) -> None:
        print(f"Lights dimmed to {level}")


class Screen:
    def up(self) -> None:
        print("Screen going up")

    def down(self) -> None:
        print("Screen going down")


class PopcornPopper:
    def on(self) -> None:
        print("Popcorn Popper on")

    def off(self) -> None:
        print("Popcorn Popper off")

    def pop(self) -> None:
        print("Popcorn Popper popping popcorn!")


class DvdPlayer:
    def on(self) -> None:
        print("DVD Player on")

    def off(self) -> None:
        print("DVD Player off")

    def play(self, movie: str) -> None:
        print(f'DVD Player playing "{movie}"')

    def stop(self) -> None:
        print("DVD Player stopped")

    def eject(self) -> None:
        print("DVD Player eject")


class HomeTheaterFaçade:
    def __init__(
        self,
        amp: Amplifier,
        dvd: DvdPlayer,
        projector: Projector,
        lights: TheaterLights,
        screen: Screen,
        popper: PopcornPopper,
    ) -> None:
        self.amp = amp
        self.dvd = dvd
        self.projector = projector
        self.lights = lights
        self.screen = screen
        self.popper = popper

    def watch_movie(self, movie: str) -> None:
        print("Get ready to watch a movie...")
        self.popper.on()
        self.popper.pop()
        self.lights.dim(10)
        self.screen.down()
        self.projector.on()
        self.projector.wide_screen_mode()
        self.amp.on()
        self.amp.set_dvd(self.dvd)
        self.amp.set_surround_sound()
        self.amp.set_volume(5)
        self.dvd.on()
        self.dvd.play(movie)

    def end_movie(self) -> None:
        print("Shutting movie theater down...")
        self.popper.off()
        self.lights.on()
        self.screen.up()
        self.projector.off()
        self.amp.off()
        self.dvd.stop()
        self.dvd.eject()
        self.dvd.off()


home_theater = HomeTheaterFaçade(
    Amplifier(), DvdPlayer(), Projector(),
    TheaterLights(), Screen(), PopcornPopper(),
)
home_theater.watch_movie("Raiders of the Lost Ark")
home_theater.end_movie()
class Amplifier {
  on(): void { console.log("Amplifier on"); }
  off(): void { console.log("Amplifier off"); }
  setDvd(dvd: DvdPlayer): void { console.log("Amplifier setting DVD player"); }
  setSurroundSound(): void { console.log("Amplifier surround sound on"); }
  setVolume(level: number): void { console.log(`Amplifier setting volume to ${level}`); }
}

class Projector {
  on(): void { console.log("Projector on"); }
  off(): void { console.log("Projector off"); }
  wideScreenMode(): void { console.log("Projector in widescreen mode"); }
}

class TheaterLights {
  on(): void { console.log("Lights on"); }
  dim(level: number): void { console.log(`Lights dimmed to ${level}`); }
}

class Screen {
  up(): void { console.log("Screen going up"); }
  down(): void { console.log("Screen going down"); }
}

class PopcornPopper {
  on(): void { console.log("Popcorn Popper on"); }
  off(): void { console.log("Popcorn Popper off"); }
  pop(): void { console.log("Popcorn Popper popping popcorn!"); }
}

class DvdPlayer {
  on(): void { console.log("DVD Player on"); }
  off(): void { console.log("DVD Player off"); }
  play(movie: string): void { console.log(`DVD Player playing "${movie}"`); }
  stop(): void { console.log("DVD Player stopped"); }
  eject(): void { console.log("DVD Player eject"); }
}

class HomeTheaterFaçade {
  constructor(
    private readonly amp: Amplifier,
    private readonly dvd: DvdPlayer,
    private readonly projector: Projector,
    private readonly lights: TheaterLights,
    private readonly screen: Screen,
    private readonly popper: PopcornPopper,
  ) {}

  watchMovie(movie: string): void {
    console.log("Get ready to watch a movie...");
    this.popper.on();
    this.popper.pop();
    this.lights.dim(10);
    this.screen.down();
    this.projector.on();
    this.projector.wideScreenMode();
    this.amp.on();
    this.amp.setDvd(this.dvd);
    this.amp.setSurroundSound();
    this.amp.setVolume(5);
    this.dvd.on();
    this.dvd.play(movie);
  }

  endMovie(): void {
    console.log("Shutting movie theater down...");
    this.popper.off();
    this.lights.on();
    this.screen.up();
    this.projector.off();
    this.amp.off();
    this.dvd.stop();
    this.dvd.eject();
    this.dvd.off();
  }
}

const homeTheater = new HomeTheaterFaçade(
  new Amplifier(),
  new DvdPlayer(),
  new Projector(),
  new TheaterLights(),
  new Screen(),
  new PopcornPopper(),
);
homeTheater.watchMovie("Raiders of the Lost Ark");
homeTheater.endMovie();

Consequences

Applying the Façade pattern leads to several architectural benefits and trade-offs:

  • Simplified Interface: The primary intent of a Façade is to simplify the interface for the client.
  • Reduced Coupling: It decouples the client from the subsystem. Because the client only interacts with the Façade, internal changes to the subsystem (like adding a new device) do not require changes to the client code.
  • Improved Information Hiding: It promotes modularity by ensuring that the low-level details of the subsystems are “secrets” kept within the component.
  • Flexibility: Clients that still need the power of the low-level interfaces can still access them directly; the Façade does not “trap” the subsystem, it just provides a more convenient way to use it for common tasks. This is a critical point: a Façade is a convenience, not a prison.

Design Decisions

Single vs. Multiple Façades

When a subsystem is large, a single Façade can become a “god class” that handles too many concerns. In such cases, create multiple facades, each responsible for a different aspect of the subsystem (e.g., HomeTheaterPlaybackFaçade and HomeTheaterSetupFaçade). This keeps each Façade cohesive and manageable.

Façade Awareness

Subsystem classes should not know about the Façade. The Façade knows the subsystem internals and delegates to them, but the subsystem components remain fully independent. This one-directional knowledge ensures the subsystem can be used without the Façade and can be tested independently.

Abstract Façade

When testability matters or when the subsystem may have platform-specific implementations, define the Façade as an interface or abstract class. The Gang of Four call this “reducing client-subsystem coupling further”: clients communicate with the subsystem through the abstract Façade interface, so they don’t know which concrete implementation of a subsystem is being used (GoF, p. 178). An alternative is to keep the Façade concrete but configure it with different subsystem objects.

Public vs. Private Subsystem Classes

A subsystem is analogous to a class: both have public and private interfaces. The Façade is part of the public interface to the subsystem, but not the only part — other classes that clients legitimately need to access (e.g., Scanner and Parser in the GoF compiler example) are also public. Classes that only subsystem extenders need are private. Languages like C++ provide namespaces to expose only the public subsystem classes; in others, this distinction is enforced by convention (GoF, p. 178).

The Principle of Least Knowledge (Law of Demeter)

Head First Design Patterns introduces the Façade pattern alongside a related design principle:

Principle of Least Knowledge — talk only to your immediate friends.

This principle (also known as the Law of Demeter) guides us to reduce the interactions between objects to just a few close “friends”. When designing a system, for any object, be careful of the number of classes it interacts with and how it comes to interact with those classes. Following this principle prevents designs where a large number of classes are coupled together so that changes in one part cascade to other parts.

The principle states that, from any method in an object, you should only invoke methods that belong to:

  1. The object itself
  2. Objects passed in as a parameter to the method
  3. Any object the method creates or instantiates
  4. Any components of the object (objects referenced by an instance variable — a “HAS-A” relationship)

A common violation is “train wreck” code that chains calls returned from other calls:

// Violates Principle of Least Knowledge — calls method on object returned from another call
public float getTemp() {
    return station.getThermometer().getTemperature();
}

// Follows the principle — Station exposes a method that hides the thermometer
public float getTemp() {
    return station.getTemperature();
}

How the Façade follows this principle. Without a Façade, the client must talk to every component of the subsystem — the amplifier, projector, lights, screen, DVD player, popcorn popper, and so on. With the Façade, the client has only one friend: the HomeTheaterFaçade. The Façade itself talks to its components (which are HAS-A relationships, satisfying rule 4), so it is also adhering to the principle. This is one of the reasons Façade reduces coupling so effectively.

Trade-off. Applying the principle often requires writing more “wrapper” methods (e.g., Station.getTemperature() that just delegates to thermometer.getTemperature()). This can result in increased complexity and development time, as well as decreased runtime performance. Like all principles, it should be applied with judgment.

Distinguishing Façade from Related Patterns

The Façade is often confused with Adapter and Mediator because all three involve intermediary objects. The distinctions are:

Pattern Intent Knowledge Direction Scope
Façade Simplify a complex subsystem into a convenient interface One-way: Façade knows the subsystem; subsystem classes have no knowledge of the Façade. Many existing interfaces → one new simpler interface
Adapter Convert an existing interface so it matches another expected interface One-way: Client calls Adapter; Adapter calls Adaptee; Adaptee is unaware. One existing interface → one expected interface (one-to-one)
Mediator Coordinate interactions between peer objects Two-way awareness: Colleagues know the Mediator and call it; the Mediator calls Colleagues back. Many peer Colleagues coordinated through one centralized object

A Façade simplifies access to a subsystem; an Adapter changes the shape of one interface to fit another; a Mediator coordinates among peers. If the intermediary hides a subsystem from outside clients (and the subsystem doesn’t know about it), it is a Façade. If it converts one interface into another, it is an Adapter. If it manages communication among peers that all know about it, it is a Mediator.

Façade vs. Abstract Factory. The Gang of Four note that Abstract Factory can be used with Façade to provide an interface for creating subsystem objects in a subsystem-independent way. Abstract Factory can also be used as an alternative to Façade to hide platform-specific classes (GoF, p. 182).

Façade is often a Singleton. Because usually only one Façade object is required for a subsystem, Façades are often implemented as Singletons (GoF, p. 183).

Flashcards

Structural Pattern Flashcards

Key concepts for Adapter, Composite, and Facade patterns.

Difficulty: Basic

What problem does Adapter solve?

Difficulty: Intermediate

Object Adapter vs. Class Adapter?

Difficulty: Intermediate

Adapter vs. Facade vs. Decorator?

Difficulty: Advanced

What does POSA5 say about ‘the Adapter pattern’?

Difficulty: Basic

What problem does Composite solve?

Difficulty: Advanced

Composite: Transparent vs. Safe design?

Difficulty: Advanced

Name three pattern compounds involving Composite.

Difficulty: Basic

What problem does Facade solve?

Difficulty: Advanced

Facade vs. Mediator: what’s the communication direction?

Difficulty: Intermediate

Should the subsystem know about its Facade?

Quiz

Structural Patterns Quiz

Test your understanding of Adapter, Composite, and Facade — their distinctions, design decisions, and when to apply each.

Difficulty: Advanced

A TurkeyAdapter implements the Duck interface. The fly() method calls turkey.fly() five times in a loop because a duck’s flight is much longer than a turkey’s short hop. What design concern does this raise?

Correct Answer:
Difficulty: Intermediate

A colleague says: “We should use an Adapter between our service and the database layer.” Your team wrote both the service and the database layer. What is the best response?

Correct Answer:
Difficulty: Intermediate

In a Composite pattern for a restaurant menu system, a developer declares add(MenuComponent) on the abstract MenuComponent class (inherited by both Menu and MenuItem). A tester calls menuItem.add(anotherItem). What happens, and what design trade-off does this illustrate?

Correct Answer:
Difficulty: Advanced

All three patterns — Adapter, Facade, and Decorator — involve “wrapping” another object. What is the key distinction between them?

Correct Answer:
Difficulty: Advanced

A HomeTheaterFacade exposes watchMovie(), endMovie(), listenToMusic(), stopMusic(), playGame(), setupKaraoke(), and calibrateSystem(). The class is growing difficult to maintain. What is the best architectural response?

Correct Answer:
Difficulty: Advanced

The Facade’s communication is one-directional: the Facade calls subsystem classes, but the subsystem does not know about the Facade. The Mediator’s communication is bidirectional. Why does this distinction matter architecturally?

Correct Answer:

Design Principles


Separation of Concerns

Description

Information Hiding

Description

SOLID

Description

Information Hiding


Background and Motivation

A Motivating Story: The PayPal Tangle

Imagine you joined a team building an online store. The first sprint went well: you shipped checkout, refunds, and a wallet. But you used PayPal directly everywhere — OrderService, RefundService, and WalletService each call PayPal.charge(...), PayPal.refund(...), paypal.authenticate(...), and so on. Every service knows that PayPal exists, knows how to authenticate to PayPal, and constructs PayPal-specific objects like PayPalCharge.

class Order {
    int total() { return 0; }
}

class PayPalAccount {
    void authenticate() { }
    String accountToken() { return ""; }
}

class PayPalCharge {
    boolean wasSuccessful() { return true; }
}

class PayPalRefund { }
class PayPalPaymentMethod { }

class PayPal {
    static PayPalCharge charge(String token, int amount) {
        return new PayPalCharge();
    }

    static PayPalRefund refund(String token, int amount) {
        return new PayPalRefund();
    }

    static PayPalPaymentMethod createPaymentMethod(String token) {
        return new PayPalPaymentMethod();
    }
}

class OrderService {
    public void checkout(Order order, PayPalAccount paypal) {
        paypal.authenticate();
        PayPalCharge charge = PayPal.charge(paypal.accountToken(), order.total());
        if (charge.wasSuccessful()) {
            // more business logic that depends on the 'charge' object ...
        } else { /* error handling */ }
    }
}

class RefundService {
    public void refund(Order order, PayPalAccount paypal) {
        paypal.authenticate();
        PayPalRefund refund = PayPal.refund(paypal.accountToken(), order.total());
        // more business logic that depends on the 'refund' object ...
    }
}

class WalletService {
    public void addPaymentMethod(PayPalAccount paypal) {
        paypal.authenticate();
        PayPalPaymentMethod payment = PayPal.createPaymentMethod(paypal.accountToken());
        // more business logic that depends on the 'payment' object ...
    }
}
#include <string>

class Order {
public:
    int total() const { return 0; }
};

class PayPalAccount {
public:
    void authenticate() { }
    std::string accountToken() const { return ""; }
};

class PayPalCharge {
public:
    bool wasSuccessful() const { return true; }
};

class PayPalRefund { };
class PayPalPaymentMethod { };

class PayPal {
public:
    static PayPalCharge charge(const std::string& token, int amount) {
        return {};
    }

    static PayPalRefund refund(const std::string& token, int amount) {
        return {};
    }

    static PayPalPaymentMethod createPaymentMethod(const std::string& token) {
        return {};
    }
};

class OrderService {
public:
    void checkout(const Order& order, PayPalAccount& paypal) {
        paypal.authenticate();
        PayPalCharge charge = PayPal::charge(paypal.accountToken(), order.total());
        if (charge.wasSuccessful()) {
            // more business logic that depends on the charge object ...
        } else { /* error handling */ }
    }
};

class RefundService {
public:
    void refund(const Order& order, PayPalAccount& paypal) {
        paypal.authenticate();
        PayPalRefund refund = PayPal::refund(paypal.accountToken(), order.total());
        // more business logic that depends on the refund object ...
    }
};

class WalletService {
public:
    void addPaymentMethod(PayPalAccount& paypal) {
        paypal.authenticate();
        PayPalPaymentMethod payment = PayPal::createPaymentMethod(paypal.accountToken());
        // more business logic that depends on the payment object ...
    }
};
class Order:
    def total(self) -> int:
        return 0


class PayPalAccount:
    def authenticate(self) -> None:
        pass

    def account_token(self) -> str:
        return ""


class PayPalCharge:
    def was_successful(self) -> bool:
        return True


class PayPalRefund:
    pass


class PayPalPaymentMethod:
    pass


class PayPal:
    @staticmethod
    def charge(token: str, amount: int) -> PayPalCharge:
        return PayPalCharge()

    @staticmethod
    def refund(token: str, amount: int) -> PayPalRefund:
        return PayPalRefund()

    @staticmethod
    def create_payment_method(token: str) -> PayPalPaymentMethod:
        return PayPalPaymentMethod()


class OrderService:
    def checkout(self, order: Order, paypal: PayPalAccount) -> None:
        paypal.authenticate()
        charge = PayPal.charge(paypal.account_token(), order.total())
        if charge.was_successful():
            # more business logic that depends on the charge object ...
            pass
        else:
            # error handling
            pass


class RefundService:
    def refund(self, order: Order, paypal: PayPalAccount) -> None:
        paypal.authenticate()
        refund = PayPal.refund(paypal.account_token(), order.total())
        # more business logic that depends on the refund object ...


class WalletService:
    def add_payment_method(self, paypal: PayPalAccount) -> None:
        paypal.authenticate()
        payment = PayPal.create_payment_method(paypal.account_token())
        # more business logic that depends on the payment object ...
class Order {
  total(): number {
    return 0;
  }
}

class PayPalAccount {
  authenticate(): void { }

  accountToken(): string {
    return "";
  }
}

class PayPalCharge {
  wasSuccessful(): boolean {
    return true;
  }
}

class PayPalRefund { }
class PayPalPaymentMethod { }

class PayPal {
  static charge(token: string, amount: number): PayPalCharge {
    return new PayPalCharge();
  }

  static refund(token: string, amount: number): PayPalRefund {
    return new PayPalRefund();
  }

  static createPaymentMethod(token: string): PayPalPaymentMethod {
    return new PayPalPaymentMethod();
  }
}

class OrderService {
  checkout(order: Order, paypal: PayPalAccount): void {
    paypal.authenticate();
    const charge = PayPal.charge(paypal.accountToken(), order.total());
    if (charge.wasSuccessful()) {
      // more business logic that depends on the charge object ...
    } else { /* error handling */ }
  }
}

class RefundService {
  refund(order: Order, paypal: PayPalAccount): void {
    paypal.authenticate();
    const refund = PayPal.refund(paypal.accountToken(), order.total());
    // more business logic that depends on the refund object ...
  }
}

class WalletService {
  addPaymentMethod(paypal: PayPalAccount): void {
    paypal.authenticate();
    const payment = PayPal.createPaymentMethod(paypal.accountToken());
    // more business logic that depends on the payment object ...
  }
}

The PayPal decision is duplicated across all three services. Each service authenticates to PayPal, calls a PayPal-specific function, and consumes a PayPal-specific result type. Visually, the dependencies look like this:

Three services, three direct dependencies on the PayPal SDK. The “secret” — which payment provider we use — is not a secret at all; every service knows it. Two months later, the CFO walks in:

“Visa is offering us better rates. Marketing wants Apple Pay for the mobile launch. Legal wants us to add Stripe for the EU rollout because PayPal won’t sign their data-processing addendum. How long?”

You open your editor, search for PayPal, and your heart sinks. The string PayPal appears in dozens of files — services, tests, error messages, retry logic, even logging. None of those files were about payment providers, but every one of them now needs to be edited. You estimate three weeks for the change, two more for regression testing, and a non-trivial probability that something subtle will break in production.

This is not a coding problem. This is a design problem. The team violated a design principle that has been known for over fifty years: a single difficult, likely-to-change design decision — which payment provider we use — was scattered across the entire codebase instead of being hidden inside a single module behind a robust interface. Every service “knew the secret”. So every service had to be rewritten when the secret changed.

The principle that fixes this is called Information Hiding. The fix looks like this:

class Order { }
class PaymentDetails { }
class ChargeResult { }
class RefundResult { }
class PaymentMethod { }

// 1. Define a vendor-neutral interface — the only contract clients see.
interface PaymentGateway {
    ChargeResult charge(Order order, PaymentDetails payment);
    RefundResult refund(Order order, PaymentDetails payment);
    PaymentMethod createPaymentMethod(PaymentDetails payment);
}

// 2. ONE module hides the PayPal decision.
class PayPalGateway implements PaymentGateway {
    // PayPalDecision lives here — and ONLY here.
    public ChargeResult charge(Order order, PaymentDetails payment) {
        return new ChargeResult();
    }

    public RefundResult refund(Order order, PaymentDetails payment) {
        return new RefundResult();
    }

    public PaymentMethod createPaymentMethod(PaymentDetails payment) {
        return new PaymentMethod();
    }
}

// 3. Services depend on the abstraction, never on PayPal.
class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public void checkout(Order order, PaymentDetails payment) {
        gateway.charge(order, payment);
        // more business logic ...
    }
}

class RefundService {
    private final PaymentGateway gateway;

    RefundService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public void refund(Order order, PaymentDetails payment) {
        gateway.refund(order, payment);
        // more business logic ...
    }
}

class WalletService {
    private final PaymentGateway gateway;

    WalletService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    public void addPaymentMethod(PaymentDetails payment) {
        gateway.createPaymentMethod(payment);
        // more business logic ...
    }
}
class Order { };
class PaymentDetails { };
class ChargeResult { };
class RefundResult { };
class PaymentMethod { };

// 1. Define a vendor-neutral interface — the only contract clients see.
class PaymentGateway {
public:
    virtual ~PaymentGateway() = default;
    virtual ChargeResult charge(const Order& order, const PaymentDetails& payment) = 0;
    virtual RefundResult refund(const Order& order, const PaymentDetails& payment) = 0;
    virtual PaymentMethod createPaymentMethod(const PaymentDetails& payment) = 0;
};

// 2. ONE module hides the PayPal decision.
class PayPalGateway : public PaymentGateway {
public:
    // PayPalDecision lives here — and ONLY here.
    ChargeResult charge(const Order& order, const PaymentDetails& payment) override {
        return {};
    }

    RefundResult refund(const Order& order, const PaymentDetails& payment) override {
        return {};
    }

    PaymentMethod createPaymentMethod(const PaymentDetails& payment) override {
        return {};
    }
};

// 3. Services depend on the abstraction, never on PayPal.
class OrderService {
public:
    explicit OrderService(PaymentGateway& gateway) : gateway(gateway) { }

    void checkout(const Order& order, const PaymentDetails& payment) {
        gateway.charge(order, payment);
        // more business logic ...
    }

private:
    PaymentGateway& gateway;
};

class RefundService {
public:
    explicit RefundService(PaymentGateway& gateway) : gateway(gateway) { }

    void refund(const Order& order, const PaymentDetails& payment) {
        gateway.refund(order, payment);
        // more business logic ...
    }

private:
    PaymentGateway& gateway;
};

class WalletService {
public:
    explicit WalletService(PaymentGateway& gateway) : gateway(gateway) { }

    void addPaymentMethod(const PaymentDetails& payment) {
        gateway.createPaymentMethod(payment);
        // more business logic ...
    }

private:
    PaymentGateway& gateway;
};
from typing import Protocol


class Order:
    pass


class PaymentDetails:
    pass


class ChargeResult:
    pass


class RefundResult:
    pass


class PaymentMethod:
    pass


# 1. Define a vendor-neutral interface — the only contract clients see.
class PaymentGateway(Protocol):
    def charge(self, order: Order, payment: PaymentDetails) -> ChargeResult: ...
    def refund(self, order: Order, payment: PaymentDetails) -> RefundResult: ...
    def create_payment_method(self, payment: PaymentDetails) -> PaymentMethod: ...


# 2. ONE module hides the PayPal decision.
class PayPalGateway:
    # PayPalDecision lives here — and ONLY here.
    def charge(self, order: Order, payment: PaymentDetails) -> ChargeResult:
        return ChargeResult()

    def refund(self, order: Order, payment: PaymentDetails) -> RefundResult:
        return RefundResult()

    def create_payment_method(self, payment: PaymentDetails) -> PaymentMethod:
        return PaymentMethod()


# 3. Services depend on the abstraction, never on PayPal.
class OrderService:
    def __init__(self, gateway: PaymentGateway) -> None:
        self._gateway = gateway

    def checkout(self, order: Order, payment: PaymentDetails) -> None:
        self._gateway.charge(order, payment)
        # more business logic ...


class RefundService:
    def __init__(self, gateway: PaymentGateway) -> None:
        self._gateway = gateway

    def refund(self, order: Order, payment: PaymentDetails) -> None:
        self._gateway.refund(order, payment)
        # more business logic ...


class WalletService:
    def __init__(self, gateway: PaymentGateway) -> None:
        self._gateway = gateway

    def add_payment_method(self, payment: PaymentDetails) -> None:
        self._gateway.create_payment_method(payment)
        # more business logic ...
class Order { }
class PaymentDetails { }
class ChargeResult { }
class RefundResult { }
class PaymentMethod { }

// 1. Define a vendor-neutral interface — the only contract clients see.
interface PaymentGateway {
  charge(order: Order, payment: PaymentDetails): ChargeResult;
  refund(order: Order, payment: PaymentDetails): RefundResult;
  createPaymentMethod(payment: PaymentDetails): PaymentMethod;
}

// 2. ONE module hides the PayPal decision.
class PayPalGateway implements PaymentGateway {
  // PayPalDecision lives here — and ONLY here.
  charge(order: Order, payment: PaymentDetails): ChargeResult {
    return new ChargeResult();
  }

  refund(order: Order, payment: PaymentDetails): RefundResult {
    return new RefundResult();
  }

  createPaymentMethod(payment: PaymentDetails): PaymentMethod {
    return new PaymentMethod();
  }
}

// 3. Services depend on the abstraction, never on PayPal.
class OrderService {
  constructor(private readonly gateway: PaymentGateway) { }

  checkout(order: Order, payment: PaymentDetails): void {
    this.gateway.charge(order, payment);
    // more business logic ...
  }
}

class RefundService {
  constructor(private readonly gateway: PaymentGateway) { }

  refund(order: Order, payment: PaymentDetails): void {
    this.gateway.refund(order, payment);
    // more business logic ...
  }
}

class WalletService {
  constructor(private readonly gateway: PaymentGateway) { }

  addPaymentMethod(payment: PaymentDetails): void {
    this.gateway.createPaymentMethod(payment);
    // more business logic ...
  }
}

The decision to use PayPal is hidden in one module (PayPalGateway). Other services don’t know that PayPal exists — they only know PaymentGateway. The class diagram below makes the new structure obvious:

When the CFO swaps providers, you write a new StripeGateway implements PaymentGateway, change a single line of dependency-injection wiring, and ship. The three services do not change at all — the diagram simply gains a second box (StripeGateway) hanging off the same interface.

The Principle

“We propose […] that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.”

— David L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM, December 1972

In modern phrasing, the Information Hiding principle says:

Design decisions that are likely to change independently should be the secrets of separate modules. The interfaces between modules should reveal as little as possible — only assumptions considered unlikely to change.

Two halves are doing work here. “Difficult or likely-to-change decisions” is the what: identify volatility before you decompose. “Hide […] from the others” is the how: make the volatile decision visible to exactly one module, and let the rest of the system reach it only through a stable interface.

The fix in our PayPal story is one module — PaymentGateway — that is the only code in the system allowed to know that PayPal exists. Every other service depends on PaymentGateway, never on PayPal. When the CFO swaps providers, exactly one module changes.

Where the Principle Comes From: A Brief History

The Software Crisis

By the mid-1960s, software had quietly become more complex than the hardware that ran it. Margaret Hamilton, lead software engineer for the Apollo missions, famously observed that “the software was more complex [than the hardware] for the manned missions”. In 1968 the NATO conference on software engineering crystallized the “Software Crisis” — the recognition that software projects were systematically late, over budget, and failing to meet specifications. Brooks would later capture the same lament in The Mythical Man-Month.

A central question came out of that conference: how do you decompose a large program so that complexity does not bury the team? For most of the 1960s the answer was: break the program into the steps of a flowchart, and make each step a module. This is the natural impulse — it mirrors how humans describe procedures. But it scales badly: when a step’s details change, every step that depended on those details breaks too.

David Parnas, 1972, and the KWIC Example

Four years after the NATO conference, David L. Parnas published a short, sharp paper titled On the Criteria To Be Used in Decomposing Systems into Modules (Parnas 1972). He took a tiny example program — the KWIC (Key Word In Context) index — and decomposed it two ways.

The KWIC system itself is small: it accepts an ordered set of lines, where each line is a sequence of words. Any line can be circularly shifted by repeatedly removing the first word and appending it to the end. The system outputs all circular shifts of all lines, sorted alphabetically. This is not just a toy — Unix’s “permuted” index for the man pages is essentially a real-world KWIC.

Parnas decomposed it two ways:

Decomposition Module = … When the data structure changes …
Conventional one step of the flowchart (read input, shift, alphabetize, print) almost every module changes, because each step knows the shared data structure
Information-hiding one design decision (e.g., “how lines are stored”, “how shifting is implemented”) only the one module that owns the decision changes

He then traced several plausible changes through both designs: changes to the processing algorithm (shift each line as it is read, vs. shift all lines at once, vs. shift lazily on demand); changes to the data representation (how lines are stored, whether circular shifts are stored explicitly or as pairs of (line, offset)); enhancements to function (filter out shifts starting with noise words like “a” and “an”; allow interactive deletion); changes to performance (space and time); and changes to reuse. The information-hiding decomposition absorbed each change inside one module; the conventional one rippled across most of the system.

Parnas’s conclusion was startling at the time:

  • Both decompositions worked, but the information-hiding one was dramatically easier to change, easier to understand independently, and easier to develop in parallel.
  • The mistake of the conventional decomposition was that it treated the processing sequence as the criterion for splitting modules — a criterion that exposed every shared assumption to every module.
  • The right criterion is: what design decisions does this module hide? A module that hides a decision no one else needs to know is a good module. A module whose existence cannot be justified by any hidden decision is a bad module.
  • A practical test for hiding: imagine two design alternatives, A and B, for some volatile decision (e.g., shift-on-read vs. shift-on-demand). If you can design the module’s interface so that both A and B are implementable behind the same API, you have hidden the decision well — you can switch later without rewriting the clients.

This paper is one of the most cited papers in all of software engineering. Many of the principles you will meet later — encapsulation, abstract data types, object-oriented design, layered architecture, dependency inversion, microservices — are direct descendants of this single argument.

The Mechanics: Modules, Secrets, and Interfaces

The Anatomy of a Module: Interface and Secret

A module is an independent unit of work. Parnas defined it as “a work assignment given to a programmer or programming team” — something one engineer (or one small team) can develop, test, and reason about in isolation. In practice a module can be a function, a class, a package, a library, a microservice, or even an entire team-owned subsystem. The granularity does not matter; what matters is the rule below.

Every module has two parts:

Part What it is Who sees it Stability
Interface The stable contract describing what the module does Visible to every client Should change rarely
Implementation (the secret) The code that fulfills the contract: data structures, algorithms, libraries used, sequence of internal steps Hidden inside the module Free to change at any time

Picture an iceberg: the small tip above water is the interface. The vast bulk below water is the implementation — the secret. The whole point is that the implementation can be anything you want, so long as the interface keeps its promises.

A familiar analogy: a wall power outlet. The interface is the standard two- or three-prong socket and the guaranteed voltage and frequency. The implementation — solar panels, a coal plant, a nuclear reactor, a wind turbine — is hidden. Your laptop charger doesn’t know, doesn’t care, and cannot be broken by a change in the power source. The grid can swap solar in at noon and switch to gas at midnight without you ever rewriting your charger.

Common Secrets Worth Hiding

Parnas’s paper was deliberately abstract, but five decades of practice have produced a recognizable list of categories of decisions that are almost always worth hiding. Use this as a checklist when you decompose a system:

  • Data structures and data formats. Whether names are stored as a String, a normalized Person record, an array of glyphs, or a row in a database. Whether IDs are integers or UUIDs.
  • Storage location. Whether information lives in memory, on a local disk, in a SQL database, in S3, in Redis, or behind a third-party API.
  • Algorithms and computational steps. A* vs. Dijkstra for routing. Quicksort vs. mergesort. Greedy vs. dynamic-programming for an optimization. Whether results are cached.
  • External dependencies — libraries, frameworks, vendors. Axios vs. Fetch. MongoDB vs. Postgres vs. Supabase. PayPal vs. Stripe vs. Braintree. OpenGL vs. Vulkan.
  • Hardware and platform details. CPU word size, byte ordering, screen resolution, file-path separators, OS-specific APIs.
  • Network protocols. REST vs. gRPC, JSON vs. Protobuf, HTTP/1.1 vs. HTTP/2 — as a transport detail. (Whether the protocol is stateful or stateless, however, is often part of the interface; see below.)
  • Internal sequence of operations. Whether a request is processed in two passes or one, whether validation runs before or after enrichment.

A useful question to ask while designing: “If I can imagine a future where this decision changes, can I draw a circle around exactly the modules that would have to change”? If the circle is small (ideally one module), the secret is well hidden. If the circle is large, the system has a structural problem you will pay for later.

Visible Contract or Secret Detail? Practice Recognizing Each

Information Hiding does not mean hide everything. Some things genuinely belong in the interface — they are promises the module makes to its clients. The skill is learning which decisions belong on which side of the line.

Try each of these before reading the answer:

Decision Decision should be… Why
Whether MortgageCalculator compounds monthly or daily Hidden Clients want a payment number; how it was computed is implementation detail. Future changes (“daily compounding for VIP customers”) shouldn’t ripple.
Whether the database is SQL or NoSQL Hidden Storage is the canonical secret. The application layer should not know.
Whether the network protocol is stateful or stateless Visible (in the contract) Statefulness changes how clients interact (do they reconnect? retransmit? carry a session token?). Clients cannot ignore it.
Whether the server is implemented in Node.js, Java, or Dart Hidden The wire protocol is the contract; the implementation language is irrelevant to the client.
Whether PayPal is the payment provider Hidden Vendors change. The interface should be PaymentGateway, not PayPalGateway.
Whether a function may throw an exception Visible Callers must handle it. A “silent” exception breaks contracts.
Whether requests are rate-limited Visible Callers need to back off. Hiding it produces mysterious failures.
Whether a list is stored as an array or a linked list Hidden A canonical Parnas example. Choose the data structure that fits, change it later if needed.

The general rule: hide what only the module needs to know to do its job; expose what callers need to know to use it correctly. Anything in between is a judgment call — and almost always the right call is “hide it until proven otherwise”.

Why Information Hiding Matters: Concrete Benefits

Information Hiding is not an aesthetic. It produces measurable outcomes that teams care about.

  1. Local change. When a hidden decision changes, exactly one module needs to be edited. The change does not ripple through the codebase, does not require a merge across teams, and does not need a full regression sweep — only the one module’s tests need to pass.
  2. Local reasoning. A developer reading OrderService does not need to load PayPal’s API, retry logic, or webhook semantics into their head. They only need the contract of PaymentGateway. Studies of professional developers find that program comprehension consumes ~58% of their time (Xia et al., 2017, IEEE TSE) — every byte of detail you can keep out of a reader’s head is real, recurring time saved.
  3. Parallel work. If PaymentGateway’s interface is fixed in week 1, two developers can work in parallel: one builds the PayPal implementation behind the interface; another builds OrderService against the interface, using a fake. Neither blocks the other.
  4. Independent testability. A module whose dependencies are abstracted behind interfaces can be tested with stubs and fakes. You do not need a real PayPal account to test OrderService — you supply a FakePaymentGateway that records what it was asked to do.
  5. Replaceability. When a vendor raises prices, a library is deprecated, or a database hits a scaling wall, the swap is bounded. The blast radius of “we’re changing payment providers” is one module instead of one codebase.

The mirror-image of these benefits is the cost of failing to hide information: the Big Ball of Mud (Foote and Yoder 1997), where unmanaged complexity leaves every module knowing every other module’s secrets, and a one-line business change requires touching dozens of files. This is the modern face of the 1968 software crisis.

Deep Modules vs. Shallow Modules

A modern extension of Parnas’s idea, due to John Ousterhout in A Philosophy of Software Design (Ousterhout 2021), is the distinction between deep and shallow modules.

Deep and shallow module comparison Deep module ✓ interface implementation (hides a LOT of complexity: data structures, algorithms, libraries, edge cases…) small interface deep impl. Shallow module ✗ interface (many methods, lots of detail exposed) impl. large interface tiny impl. Hides little. Reader pays the cost of a wide interface and gets almost no abstraction in return.
  • A deep module hides a lot of complexity behind a small interface. Examples: the file system (open, read, write, close — and behind it, hundreds of thousands of lines that handle disks, caching, journaling, permissions, network mounts); a garbage collector (new — and a sophisticated runtime behind it); a TCP socket.
  • A shallow module exposes a wide interface that hides little. Pass-through getters and setters, classes whose methods one-to-one delegate to another class, “service” classes with twenty methods that each do one trivial thing. The reader pays the cost of learning a new interface but gains almost no abstraction.

Deep modules are the goal of Information Hiding. Each method on the interface should “buy” the reader a meaningful chunk of hidden complexity. Shallow modules — even if every field is private — give you the worst of both worlds: more vocabulary to learn, and no actual hiding.

A simple heuristic: the bigger the difference between the interface size and the implementation size, the deeper the module. Deep modules are valuable. Shallow modules are tax.

Coupling and Cohesion: The Metrics of Hiding

Information Hiding is the principle; coupling and cohesion are the metrics that measure how well you applied it.

  • Coupling = the strength of dependencies between modules. Lower is better. Two modules are tightly coupled if a small change in one usually requires changes in the other.
  • Cohesion = the strength of dependencies within a module. Higher is better. A cohesive module’s methods all serve a single, focused purpose.

When secrets are well hidden, coupling drops (because clients only know the interface) and cohesion rises (because everything in a module exists to support that one hidden decision). When secrets leak, the opposite happens.

Aspect High Coupling, Low Cohesion (bad) Low Coupling, High Cohesion (good)
Change Ripples through many modules Stays inside one module
Understanding You must load many modules into memory at once You can reason about one module in isolation
Testing Hard to test in isolation; needs many real dependencies Easy to test with fakes
Reuse Cannot extract one part without dragging others along Modules are self-contained and portable

Not All Dependencies Are Obvious

Coupling has two flavors, and the second is the dangerous one:

  • Syntactic dependency: Module A won’t compile without Module B — it imports B, names B’s types, calls B’s methods. Easy for a tool to detect.
  • Semantic dependency: Module A won’t function correctly without Module B, even though A doesn’t name B. A and B might both implement the same hidden assumption — for example, two modules that both assume “phone numbers are stored as 10-digit strings without formatting”. If you change the assumption in one, the other silently breaks.

Semantic coupling is the reason “we’ll just refactor it later” is so often wrong: the syntactic coupling is gone but the shared assumptions are still scattered. Information Hiding fights both — but semantic coupling only goes away when the shared assumption itself lives in exactly one place.

Information Hiding ≠ Encapsulation ≠ “Make It Private”

This is the most common misconception about Information Hiding, and it is worth lingering on.

“If I make all my fields and methods private, I’m doing information hiding”.

No. Visibility modifiers (private, protected, public) are a small language tool that helps you hide things. Information Hiding is the broader design principle of choosing what should be hidden in the first place. You can violate Information Hiding while having no public fields anywhere:

// Every field is private. The class is still leaking PayPal as a "secret".
class OrderService {
    private final PayPalClient paypal;          // <-- the secret is in the field type
    private PayPalAuthToken token;              // <-- and in this type

    OrderService(PayPalClient paypal) {
        this.paypal = paypal;
    }

    public PayPalCharge checkout(Order order, PayPalAccount account) {
        token = paypal.authenticate(account);
        return paypal.charge(order.total(), token);
    }
}
// Every field is private. The class is still leaking PayPal as a "secret".
class OrderService {
public:
    explicit OrderService(PayPalClient& paypal) : paypal(paypal) { }

    PayPalCharge checkout(const Order& order, const PayPalAccount& account) {
        token = paypal.authenticate(account);
        return paypal.charge(order.total(), token);
    }

private:
    PayPalClient& paypal;   // <-- the secret is in the field type
    PayPalAuthToken token;  // <-- and in this type
};
# Naming a field with a leading underscore is only a convention.
# The class is still leaking PayPal as a "secret".
class OrderService:
    def __init__(self, paypal: "PayPalClient") -> None:
        self._paypal = paypal          # <-- the secret is in the field type
        self._token: "PayPalAuthToken | None" = None

    def checkout(self, order: "Order", account: "PayPalAccount") -> "PayPalCharge":
        self._token = self._paypal.authenticate(account)
        return self._paypal.charge(order.total(), self._token)
// Every field is private. The class is still leaking PayPal as a "secret".
class OrderService {
  private token?: PayPalAuthToken; // <-- the secret is in this type

  constructor(
    private readonly paypal: PayPalClient, // <-- and in the field type
  ) { }

  checkout(order: Order, account: PayPalAccount): PayPalCharge {
    const token = this.paypal.authenticate(account);
    this.token = token;
    return this.paypal.charge(order.total(), token);
  }
}

private did not save us. The PayPal decision is still woven into OrderService’s interface — the parameter types and return types of its public methods. Anyone who calls checkout learns that PayPal exists. The fix is to invent a PaymentGateway abstraction and let the interface of OrderService mention only that abstraction.

A better way to remember the distinction:

Term What it means
Information Hiding A design principle: identify volatile decisions and hide each one inside one module.
Encapsulation A language mechanism: bundle data and the operations on it into a single unit (a class).
Access modifiers (private, protected, public) A language tool: restrict who can call which member. Used as one of many tools to enforce encapsulation.
Abstraction A thinking technique: reason about something using only the properties relevant to your purpose. The interface of a hidden module is an abstraction.

You need all four in the toolbox. The principle (Information Hiding) tells you what to do; the mechanisms (encapsulation, access modifiers, abstraction) help you enforce it.

Applying and Evaluating Information Hiding

How Information Hiding Relates to Other Concepts

Students often confuse Information Hiding with neighboring ideas. Drawing the distinctions sharpens your ability to apply each.

Concept What it says Relationship to Information Hiding
Separation of Concerns Divide the system into distinct sections, each addressing a separate concern. SoC tells you which aspects to separate; Information Hiding tells you how to protect each separated decision behind a stable interface.
Modularity Split a system into independent work units. Modularity is the act of splitting; Information Hiding is the criterion for splitting well (split along volatile decisions).
Encapsulation Bundle data and operations into a single unit. The language mechanism most often used to enforce Information Hiding. You can encapsulate without hiding (everything public); you can hide without language-level encapsulation (a Python module with leading-underscore conventions).
Abstraction Reason about something via only its essential properties. A module’s interface is an abstraction; Information Hiding is what makes the abstraction trustworthy.
Single Responsibility (SRP) A class should have one reason to change. SRP is Information Hiding restated for the class level — one class hides one secret, so it has one reason to change.
Dependency Inversion (DIP) High-level policy depends on abstractions; details depend on those abstractions. DIP is the mechanism most commonly used to keep secrets hidden across architectural layers.
Low Coupling / High Cohesion Modules should depend on each other little, and contain related things. The metrics by which you measure whether Information Hiding succeeded.
Open/Closed Principle (OCP) Open for extension, closed for modification. When secrets are well hidden, adding a new variant (e.g., StripeGateway) extends the system without modifying any existing module — the OCP payoff.

A useful slogan, attributed to Robert C. Martin: “Gather together the things that change for the same reasons. Separate those things that change for different reasons”. That single sentence captures Information Hiding, SRP, and SoC simultaneously.

Mechanisms for Hiding

Knowing what to hide is one skill; knowing the moves to actually hide it is another. The recurring mechanisms:

  1. Interfaces and abstract types. Define a contract (PaymentGateway) and write all clients against it; let one concrete class (PayPalGateway) implement it. The decision “we use PayPal” lives in exactly one file plus the dependency-injection wiring.
  2. Dependency Inversion. Don’t reach down into low-level modules from high-level ones. Define the abstraction the high-level module needs and let the low-level module implement it. (See DIP.)
  3. Facade pattern. Wrap a complex subsystem behind a simple interface; clients see only the facade. Common when a third-party library is itself a tangled mess.
  4. Adapter pattern. Wrap an external API in your own interface so the rest of the code is insulated from its quirks.
  5. Repository / Gateway pattern. Hide the storage decision (SQL? NoSQL? in-memory?) behind a domain-shaped interface (OrderRepository.findById(id)).
  6. Modules, packages, namespaces. The crudest mechanism — putting things in different files and folders — already provides a unit of hiding, especially when paired with strong language-level visibility.
  7. Access modifiers. private, protected, internal-only modules in Rust/Go/Swift, JavaScript closures. The enforcement layer that prevents accidental leakage.
  8. Abstract data types (ADTs). Define a type by its operations, not its representation. The original tool Parnas’s followers (Liskov, Guttag) developed to operationalize the principle.

You will rarely use only one of these. A good design typically composes several: an OrderService depends on a PaymentGateway interface (mechanism 1 + 2); the concrete PayPalGateway is a facade (3) over the messy PayPal SDK; the SDK is itself adapted (4) so swapping it out is bounded; the whole thing lives in a payments/ package whose exports are restricted (6 + 7).

Change Impact Analysis: Evaluating Whether Your Design Hides Well

Information Hiding is verified by simulating change. The procedure, used in industry as change impact analysis:

  1. List the changes that could plausibly happen. New payment providers. New currencies. A migration from SQL to NoSQL. A change in regulatory requirements. Brainstorm widely; the discipline of listing forces realism.
  2. Estimate the likelihood of each. Some are inevitable (libraries get deprecated); some are speculative (a 10× traffic spike).
  3. For each likely change, count the modules that would have to change. Ideally one. If many, the secret is leaking.
  4. Redesign until no change is both highly likely and highly expensive. You will not eliminate every tail risk — but you should not be one likely change away from a re-architecture.

This is also the procedure to apply when reviewing somebody else’s design: open the code, pick a plausible future change, and trace what would have to be edited. A well-hidden design lights up one module; a poorly-hidden one lights up the whole tree.

A Five-Step Method for Applying Information Hiding

When you are designing (or reviewing) a module, run this checklist:

  1. List the secrets. What design decisions does this module own? Whether it stores its data as an array vs. a tree; which library it uses; the algorithm; the data format. If you cannot list any secret, the module probably should not exist on its own.
  2. Verify each secret is owned in exactly one place. If two modules both “know” the secret, they are semantically coupled. Pick one.
  3. Inspect the interface for leaks. Read every public method signature. Does any parameter type, return type, or thrown exception name a vendor, a database, a library, or a low-level data structure? If yes, the secret has leaked into the contract.
  4. Simulate a likely change. Pick a realistic future change and trace what would need to be edited. If the answer is more than this module, redesign.
  5. Check for shallowness. Is the implementation behind the interface non-trivial? If your “module” is a thin pass-through, merge it back into its caller — you have added an interface without buying any hiding.

When NOT to Apply Information Hiding (Trade-offs Are Real)

Like every design principle, mindless application of Information Hiding produces its own pain.

  • Throwaway scripts. A 50-line cron job does not need a PaymentGateway abstraction in front of a print statement. Hiding decisions you will never change is wasted ceremony.
  • Single-variant systems with stable scope. If there will be exactly one database forever — and you are sure of it — a thin abstraction over it is overhead.
  • Premature abstraction. Inventing a PaymentGateway when you know exactly one provider, in a domain you don’t yet understand, will usually draw the seam in the wrong place. Wait for the second variant to materialize, then refactor to the abstraction. (See Refactoring to Patterns, Kerievsky 2004.)
  • Performance-critical inner loops. Indirection has a cost — usually negligible, but occasionally measurable in tight loops or microservices boundaries. Sometimes you fuse layers deliberately for speed and comment loudly about why.
  • When the “secret” is actually part of the contract. If callers genuinely need to know the property (e.g., whether a network protocol is stateful), hiding it produces mysterious bugs. Hiding the wrong thing is worse than hiding nothing.

The SE maxim: the right number of abstractions is the smallest number that lets the system change gracefully. Beyond that number, every extra layer is a tax paid in indirection, file count, and cognitive load.

Anti-Patterns: What Poor Information Hiding Looks Like

Recognizing failure is half the skill.

  • Vendor name in the interface. OrderService.checkoutWithPayPal(...), UserRepository.saveToMongo(...), Logger.logToSplunk(...). The vendor is now part of the contract. Renaming the method when you switch vendors won’t help — you’ll have to rewrite every caller.
  • Returning the implementation type. A repository method that returns MySQLResultSet instead of List<Order>. Every caller now depends on MySQL.
  • Leaky abstractions. A “database-agnostic” Repository interface whose methods accept raw SQL fragments as strings. The interface pretends to hide the database; the parameters say otherwise.
  • Exposed mutable internals. Returning a reference to an internal List instead of an immutable view. Callers can now mutate the module’s state without going through its interface.
  • God classes. A single class with thirty fields and a hundred methods. By construction, it cannot have a small set of secrets — it has too many.
  • Shallow modules. A “service” class whose every method is a one-line pass-through to another class. The reader pays the cost of two interfaces and gets the abstraction value of one.
  • Conditional types in clients. if (paymentProvider == "paypal") { ... } else if (paymentProvider == "stripe") { ... } scattered across the code. The provider is supposed to be hidden — but every site that branches on it is implicitly knowing the secret. Replace with polymorphism.
  • Documentation as a substitute for hiding. A long comment explaining “this method is fragile because internally it depends on the order being stored as a list, please don’t change it”. If a secret has to be documented to clients, it has not been hidden.

Predict-Before-You-Read: Spot the Violation

For each snippet, silently identify which secret is leaking before reading the analysis.

Snippet A — “private” is not enough

class OrderService {
    private final PayPalClient paypal;
    private PayPalAuthToken token;

    OrderService(PayPalClient paypal) {
        this.paypal = paypal;
    }

    public PayPalCharge checkout(Order o, PayPalAccount acc) {
        token = paypal.authenticate(acc);
        return paypal.charge(o.getTotal(), token);
    }
}

Analysis: The fields are private, but the field type and the public method signature still name PayPalClient, PayPalAccount, and PayPalCharge. The PayPal decision has leaked into the contract — every caller of checkout now compiles against PayPal. Replace with a PaymentGateway abstraction that exposes only neutral types.

Snippet B — leaky storage

import sqlite3


class UserRepository:
    def __init__(self, connection: sqlite3.Connection) -> None:
        self.connection = connection
        self.connection.row_factory = sqlite3.Row

    def find_by_email(self, email: str) -> list[sqlite3.Row]:
        return self.connection.execute(
            "SELECT * FROM users WHERE email=?", (email,)
        ).fetchall()  # returns a list of sqlite3.Row

Analysis: The method signature looks abstract, but the return value is a sqlite3.Row — a SQLite-specific type. Every caller is now coupled to SQLite. Map to a domain object (User) before returning.

Snippet C — clean

from typing import Protocol


class PaymentGateway(Protocol):
    def charge(self, order: Order, payment: PaymentDetails) -> ChargeResult: ...
    def refund(self, charge_id: ChargeId) -> RefundResult: ...

class OrderService:
    def __init__(self, gateway: PaymentGateway) -> None:
        self._gateway = gateway
    def checkout(self, order: Order, payment: PaymentDetails) -> ChargeResult:
        return self._gateway.charge(order, payment)

Analysis: The vendor name appears nowhere in OrderService. Swapping providers means writing a new PaymentGateway implementation and changing the dependency-injection wiring; no service code is touched. The secret is hidden in exactly one place — the concrete gateway implementation.

Common Misconceptions

  • “Make it private and you’re done”. Visibility modifiers are one tool. Private fields whose types expose the vendor still leak. (See snippet A above.)
  • “Information Hiding is the same as Encapsulation”. Encapsulation is a mechanism; Information Hiding is the principle that decides what to encapsulate. You can encapsulate the wrong things.
  • “More layers = more hiding”. Stacking facades on facades is shallow-module-ism. Each layer must hide something — otherwise it just adds vocabulary.
  • “Hide everything”. Some decisions belong in the contract (statefulness, error behavior, rate limits). Hiding them produces silent failures or unusable APIs.
  • “Once decided, the secrets list never changes”. Reality: as the system evolves, what was once stable becomes volatile (e.g., “we will always be on AWS”). Re-evaluate the secrets when the change pressure arrives.
  • “Microservices automatically hide information”. A microservice with a 50-method REST API exposing every internal field is a distributed God Class. Service boundaries do not magically produce small interfaces; you still have to design them.

Summary

  • Information Hiding decomposes a system by design decisions, not by processing steps. Each module owns one likely-to-change decision and hides it from the rest of the system.
  • Coined by Parnas (Parnas 1972) in response to the Software Crisis, it is the foundational principle behind modern modularity, encapsulation, abstract data types, and most of OOP.
  • Every module has a stable interface (the public contract) and a hidden implementation (the secret). Clients depend on the interface; the implementation is free to change.
  • Common secrets include data structures, storage, algorithms, libraries, hardware, and processing sequence. Some things — statefulness, rate limits, exception behavior — belong in the interface.
  • Deep modules hide a lot of complexity behind a small interface. Shallow modules add overhead without value.
  • Coupling and cohesion are the metrics by which Information Hiding is measured. Low coupling, high cohesion = secrets are well hidden.
  • Information Hiding is not the same as private. Visibility modifiers are tools; Information Hiding is the principle that tells you what to hide.
  • Verify a design with change impact analysis: simulate plausible changes and count the modules that would need to change.
  • Don’t over-apply: throwaway scripts, single-variant systems, and hot inner loops sometimes pay the cost of hiding without enjoying the benefit.

Further Reading and Practice

Further Reading

  • David L. Parnas. “On the Criteria To Be Used in Decomposing Systems into Modules”. Communications of the ACM, 15(12), 1053–1058. December 1972. — The original paper. Short, sharp, and one of the most-cited papers in software engineering.
  • John K. Ousterhout. A Philosophy of Software Design (2nd ed.). Yaknyam Press, 2021. — The contemporary treatment. Coined the deep / shallow module distinction.
  • Robert C. Martin. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall, 2017. — Connects Information Hiding to SRP, DIP, and modern architecture.
  • Frederick P. Brooks Jr. The Mythical Man-Month (Anniversary ed.). Addison-Wesley, 1995. — The classic essays on the Software Crisis and “No Silver Bullet”.
  • Brian Foote and Joseph Yoder. “Big Ball of Mud”. Proceedings of the 4th Pattern Languages of Programs Conference, 1997. — What systems look like when Information Hiding is abandoned.
  • Xin Xia, Lingfeng Bao, David Lo, Zhenchang Xing, Ahmed E. Hassan, Shanping Li. “Measuring Program Comprehension: A Large-Scale Field Study with Professionals”. IEEE Transactions on Software Engineering, 44(10), 951–976, 2018. — Source for the “developers spend ~58% of their time on program comprehension” finding.
  • Joshua Kerievsky. Refactoring to Patterns. Addison-Wesley, 2004. — On evolving abstractions only when the change pressure proves you need them.

Practice

Test your understanding below. Effortful retrieval is exactly what builds durable mental models. Come back tomorrow for the spacing benefit.

Reflection Questions

  1. Pick a class or module in a codebase you’ve worked on. List the secrets it owns. If you cannot list any, what is its justification for existing as a separate module?
  2. The lecture argues that “If I make my fields private, I have hidden the data”. Why is this only half right? Give a small code example where every field is private but Information Hiding is still violated.
  3. Think of the operating system you use daily. Name two difficult or likely-to-change design decisions an OS hides (e.g., the file system, the scheduler) and describe what would happen to user programs if those decisions stopped being hidden.
  4. Some properties of a module belong in its interface, not in its hidden implementation — for example, whether a network protocol is stateful or stateless. Why? What makes a property “interface material” rather than “secret material”?
  5. The lecture mentions that “program comprehension takes up 58% of professional developers’ time”. Connect this statistic to the design decisions you make as a programmer: what kinds of information hiding most directly reduce cognitive load on future readers?

Information Hiding Flashcards

Key definitions, examples, trade-offs, and misconceptions of the Information Hiding principle (Parnas 1972).

Difficulty: Basic

State the Information Hiding principle in one sentence.

Difficulty: Basic

Who introduced the Information Hiding principle, and in what year and venue?

Difficulty: Intermediate

What two example modularizations did Parnas compare in his 1972 paper, and which won?

Difficulty: Basic

Define a module in the Parnas sense.

Difficulty: Basic

Name the two parts every module has, and which one should be stable.

Difficulty: Intermediate

Give five categories of design decisions that are commonly worth hiding inside a module.

Difficulty: Intermediate

What is the difference between a deep module and a shallow module?

Difficulty: Intermediate

True or false: ‘If I make all my fields and methods private, I have followed the Information Hiding principle.’

Difficulty: Basic

Define coupling and cohesion, and say which way each should go.

Difficulty: Advanced

Distinguish syntactic and semantic coupling. Why is the second one more dangerous?

Difficulty: Intermediate

In the lecture’s payment-system example, what is the secret, and where should it live?

Difficulty: Advanced

Why is whether a network protocol is stateful or stateless part of the interface, not the secret?

Difficulty: Advanced

What is change impact analysis, and how does it test whether your design follows Information Hiding?

Difficulty: Advanced

Name three common anti-patterns of poor Information Hiding.

Difficulty: Advanced

When is applying Information Hiding a bad idea?

Difficulty: Advanced

How does Information Hiding relate to Separation of Concerns (SoC)?

Information Hiding Quiz

Test your ability to identify, apply, and evaluate the Information Hiding principle in real code.

Difficulty: Basic

Who introduced the Information Hiding principle, and in what paper?

Correct Answer:
Difficulty: Intermediate

In Parnas’s KWIC (Key Word In Context) example, what was wrong with the conventional decomposition (one module per processing step)?

Correct Answer:
Difficulty: Advanced

Look at this Java code:

public class OrderService {
    private final PayPalClient paypal;
    public PayPalCharge checkout(Order o, PayPalAccount acc) {
        paypal.authenticate(acc);
        return paypal.charge(acc.getAccountToken(), o.getTotal());
    }
}

Every field is private. Is this an example of good Information Hiding?

Correct Answer:
Difficulty: Basic

What does Ousterhout mean by a deep module?

Correct Answer:
Difficulty: Intermediate

A teammate proposes splitting a 30-line helper function into its own class with a one-method interface, “for Information Hiding.” When is this most likely the wrong move?

Correct Answer:
Difficulty: Advanced

Which of the following is most likely to be part of the interface (visible) rather than a hidden secret?

Correct Answer:
Difficulty: Advanced

Which statement best captures the relationship between Information Hiding and Separation of Concerns (SoC)?

Correct Answer:
Difficulty: Intermediate

The CFO announces that PayPal will be replaced with Stripe. In a codebase that follows Information Hiding well, what is the expected scope of the change?

Correct Answer:
Difficulty: Basic

Which is the strongest evidence that a module is shallow?

Correct Answer:
Difficulty: Advanced

Two modules in your codebase both depend on the assumption “phone numbers are stored as exactly 10 digits, no separators.” There is no shared constant, no shared validator — just two pieces of code that happen to assume the same thing. What is this?

Correct Answer:
Difficulty: Intermediate

You inherit a UserRepository whose findByEmail method returns sqlite3.Row. Why is this a problem?

Correct Answer:
Difficulty: Advanced

In change impact analysis, what does it mean if a single plausible change (say, “we switch from JSON to Protobuf for our wire format”) would force edits across dozens of unrelated modules?

Correct Answer:
Difficulty: Basic

Which of the following is not a typical mechanism for enforcing Information Hiding?

Correct Answer:
Difficulty: Intermediate

Why does Information Hiding reduce cognitive load on developers reading code?

Correct Answer:
Difficulty: Advanced

A reviewer says: “Don’t add an abstraction for this — we only have one database and we’ll never have another.” When is this argument most reasonable?

Correct Answer:

Pedagogical tip: Try to explain each concept out loud — to a teammate, a rubber duck, or your imaginary future self — before peeking at the answer. The “generation effect” strengthens memory more than re-reading ever will.

Software Process


Agile

For decades, software development was dominated by the Waterfall model, a sequential process where each phase—requirements, design, implementation, verification, and maintenance—had to be completed entirely before the next began. This “Big Upfront Design” approach assumed that requirements were stable and that designers could predict every challenge before a single line of code was written. However, this led to significant industry frustrations: projects were frequently delayed, and because customer feedback arrived only at the very end of the multi-year cycle, teams often delivered products that no longer met the user’s changing needs.

In Waterfall, feedback from the customer only appears at the very end — after months or years of work:

Agile inverts this: the team delivers a small working increment every one to four weeks and lets customer feedback reshape each subsequent iteration — the feedback loop closes in weeks, not years.

Agile Manifesto

In 2001, a group of software experts met in Utah to address these failures, resulting in the Agile Manifesto. Rather than a rigid rulebook, the manifesto proposed a shift in values:

  • Individuals and interactions over processes and tools
  • Working software over comprehensive documentation
  • Customer collaboration over contract negotiation
  • Responding to change over following a plan While the authors acknowledged value in the items on the right, they insisted that the items on the left were more critical for success in complex environments.

Core Principles

The heart of Agility lies in iterative and incremental development. Instead of one long cycle, work is broken into short, time-boxed periods—often called Sprints—typically lasting one to four weeks. At the end of each sprint, the team delivers a “Working Increment” of the product, which is demonstrated to the customer to gather rapid feedback. This ensures the team is always building the “right” system and can pivot if requirements evolve. Key principles supporting this include:

  • Customer Satisfaction: Delivering valuable software early and continuously.
  • Simplicity: The art of maximizing the amount of work not done.
  • Technical Excellence: Continuous attention to good design to enhance long-term agility.
  • Self-Organizing Teams: Empowering developers to decide how to best organize their own work rather than acting as “coding monkeys”.

Common Agile Processes

The most common agile processes include:

  • Scrum: The most popular framework using roles like Scrum Master, Product Owner, and Developers.
  • Extreme Programming (XP): Focused on technical excellence through “extreme” versions of good practices, such as Test-Driven Development (TDD), Pair Programming, Continuous Integration, and Collective Code Ownership
  • Lean Software Development: Derived from Toyota’s manufacturing principles, Lean focuses on eliminating waste

Scrum


0:00
--:--

Audio transcript: The Scrum Theory section below is an equivalent text alternative for this audio summary.

While many organizations claim to be “Agile”, the vast majority — historically reported around 60–80% in the annual State of Agile surveys — implement the Scrum framework or a Scrum/Kanban hybrid.

Scrum Theory

Scrum is a management framework built on the philosophy of Empiricism. This philosophy asserts that in complex environments like software development, we cannot rely on detailed upfront predictions. Instead, knowledge comes from experience, and decisions must be based on what is actually observed and measured in a “real” product.

To make empiricism actionable, Scrum rests on three core pillars:

  • Transparency: Significant aspects of the process must be visible to everyone responsible for the outcome. “The work is on the wall”, meaning stakeholders and developers alike should see exactly where the project stands via Scrum’s three artifacts — the Product Backlog, Sprint Backlog, and Increment — typically displayed on a shared task board.
  • Inspection: The team must frequently and diligently check their progress toward the Sprint Goal to detect undesirable variances.
  • Adaptation: If inspection reveals that the process or product is unacceptable, the team must adjust immediately to minimize further issues. It is important to realize that Scrum is not a fixed process but one designed to be tailored to a team’s specific domain and needs.

Scrum Roles

0:00
--:--

Audio transcript: The Scrum Roles section below is an equivalent text alternative for this audio summary.

Scrum defines three specific roles — called accountabilities in the 2020 Scrum Guide (Schwaber and Sutherland 2020) — that are intentionally designed to exist in tension to ensure both speed and quality:

  • The Product Owner (The Value Navigator): This role is responsible for maximizing the value of the product resulting from the team’s work. They “own” the product vision, prioritize the backlog, and typically communicate requirements through user stories.
  • The Developers (The Builders): Developers in Scrum are meant to be cross-functional and self-organizing. This means they possess all the skills needed—UI, backend, testing—to create a usable increment without depending on outside teams. They are responsible for adhering to a Definition of Done to ensure internal quality.
  • The Scrum Master (The Coach): Misunderstood as a “project manager”, the Scrum Master is actually a servant-leader. Their primary objective is to maximize team effectiveness by removing “impediments” (blockers like legal delays or missing licenses) and coaching the team on Scrum values.

Scrum Artifacts

Scrum manages work through three primary artifacts:

  • Product Backlog: An emergent, ordered list of everything needed to improve the product.
  • Sprint Backlog: A subset of items selected for the current iteration, coupled with an actionable plan for delivery.
  • The Increment: A concrete, verified stepping stone toward the Product Goal. An increment is only “born” once a backlog item meets the team’s Definition of Done—a checklist of quality measures like functional testing, documentation, and performance benchmarks.

Scrum Events

The framework follows a specific rhythm of time-boxed events:

  • The Sprint: A timeboxed period of one month or less (typically 1–4 weeks) that contains all the other Scrum events. Sprints are fixed-length and start immediately after the previous one ends.
  • Sprint Planning: The entire team collaborates to define why the sprint is valuable (the goal), what can be done, and how it will be built.
  • Daily Standup (Daily Scrum): A 15-minute event where Developers inspect progress toward the Sprint Goal and adjust their plan for the next day. (Earlier versions of Scrum prescribed three questions — what was done, what will be done, and obstacles — but the 2020 Scrum Guide removed this prescription, leaving the Developers free to choose whatever structure works for them.)
  • Sprint Review: A working session at the end of the sprint where stakeholders provide feedback on the working increment. A good review includes live demos, not just slides.
  • Sprint Retrospective: The team reflects on their process and identifies ways to increase future quality and effectiveness.

The sprint is a closed feedback loop: every event feeds the next, and the retrospective loops the team back into the next planning session.

The retrospective’s arrow back to planning is the engine of empiricism: each cycle the team inspects both the product (in review) and the process (in retro), and adapts before the next sprint starts.

Scaling Scrum with SAFe

When a product is too massive for a single Scrum Team (typically 10 or fewer people, per the 2020 Scrum Guide), organizations often use the Scaled Agile Framework (SAFe). SAFe introduces the Agile Release Train (ART)—a “team of teams” that synchronizes their sprints. It operates on Program Increments (PI), typically lasting 8–12 weeks, which align multiple teams toward quarterly goals. While SAFe provides predictability for Fortune 500 companies, critics sometimes call it “Scrum-but-for-managers” because it can reduce individual team autonomy through heavy planning requirements.

Practice

Scrum Quiz

Recalling what you just learned is the best way to form lasting memory. Use this quiz to test your understanding of the Scrum framework, roles, events, and principles.

Difficulty: Intermediate

A software development group realizes their newest feature is confusing users based on early behavioral data. They immediately halt their current plan to redesign the user interface. Which foundational philosophy of their framework does this best illustrate?

Correct Answer:
Difficulty: Intermediate

In an environment that prioritizes agility, the individuals actually building the product must possess a specific dynamic. Which description best captures how this group should operate?

Correct Answer:
Difficulty: Basic

The development group is completely blocked because they lack access to a third-party API required for their current iteration. Who is primarily responsible for facilitating the resolution of this organizational bottleneck?

Correct Answer:
Difficulty: Basic

To ensure the team is consistently tackling the most crucial problems first, someone must dictate the priority of upcoming work items. Who holds this responsibility?

Correct Answer:
Difficulty: Intermediate

What condition must be strictly satisfied before a newly developed feature is officially considered a completed, verifiable stepping stone toward the ultimate product vision?

Correct Answer:
Difficulty: Intermediate

What is the primary objective of the Daily Scrum?

Correct Answer:
Difficulty: Basic

At the conclusion of a work cycle, the team gathers specifically to discuss how they can improve their internal collaboration and technical practices for the next cycle. Which event does this describe?

Correct Answer:
Difficulty: Advanced

When a massive enterprise needs to coordinate dozens of teams working on the same vast product, they might adopt a ‘team of teams’ approach. According to common critiques, what is a potential drawback of this heavily synchronized model?

Correct Answer:

Extreme Programming (XP)


Overview

Extreme Programming, or XP, emerged as one of the most influential Agile frameworks, originally proposed by software expert Kent Beck. Unlike traditional “Waterfall” models that rely on “Big Upfront Design” and assume stable requirements, XP is built for environments where requirements evolve rapidly as the customer interacts with the product. The core philosophy is to identify software engineering practices that work well and push them to their purest, most “extreme” form.

The primary objectives of XP are to maximize business value, embrace changing requirements even late in development, and minimize the inherent risks of software construction through short, feedback-driven cycles.

Applicability and Limitations

XP is specifically designed for small teams (ideally 4–10 people) located in a single workspace where working software is needed constantly. While it excels at responsiveness, it is often difficult to scale to massive organizations of thousands of people, and it may not be suitable for systems like spacecraft software where the cost of failure is absolute and working software cannot be “continuously” deployed in flight.

XP Practices

The success of XP relies on a set of loosely coupled practices that synergize to improve software quality and team responsiveness.

The Planning Game (and Planning Poker)

The goal of the Planning Game is to align business needs with technical capabilities. It involves two levels of planning:

  • Release Planning: The customer presents user stories, and developers estimate the effort required. This allows the customer to prioritize features based on a balance of business value and technical cost.
  • Iteration Planning: User stories are broken down into technical tasks for a short development cycle (usually 1–4 weeks).

To facilitate estimation, teams often use Planning Poker. Each member holds cards with Fibonacci numbers representing “story points”—imaginary units of effort. If estimates differ wildly, the team discusses the reasoning (e.g., a hidden complexity or a helpful library) until a consensus is reached.

Small Releases

XP teams maximize customer value by releasing working software early, often, and incrementally. This provides rapid feedback and reduces risk by validating real-world assumptions in short cycles rather than waiting years for a final delivery.

Test-Driven Development (TDD)

In XP, testing is not a final phase but a continuous activity. TDD follows a strict “Red-Green-Refactor” rhythm:

  • Red: Write a tiny, failing test for a new requirement.
  • Green: Write the simplest possible code to make that test pass, even taking shortcuts.
  • Refactor: Clean the code and improve the design while ensuring the tests still pass.

TDD ensures high test coverage and results in “living documentation” that describes exactly what the code should do.

Pair Programming

Two developers work together on a single machine. One acts as the Driver (hands on the keyboard, focusing on local implementation), while the other is the Navigator (watching for bugs and thinking about the high-level architecture). Research suggests this improves product quality, reduces risk, and aids in knowledge management.

Continuous Integration (CI)

To avoid the “integration hell” that occurs when developers wait too long to merge their work, XP mandates integrating and testing the entire system multiple times a day. A key benchmark is the 10-minute build: if the build and test process takes longer than 10 minutes, the feedback loop becomes too slow.

Collective Code Ownership

In XP, there are no individual owners of modules; the entire team owns all the code. This increases the bus factor—the number of people who can disappear before the project stalls—and ensures that any team member can fix a bug or improve a module.

Coding Standards

To make collective ownership feasible, the team must adhere to strict coding standards so that the code looks unified, regardless of who wrote it. This reduces the cognitive load during code reviews and maintenance.

Critical Perspectives: Design vs. Agility

A common critique of XP is that focusing solely on implementing features can lead to a violation of the Information Hiding principle. Because TDD focuses on the immediate requirements of a single feature, developers may fail to step back and structure modules around design decisions likely to change.

To mitigate this, XP advocates for “Continuous attention to technical excellence”. While working software is the primary measure of progress, a team that ignores good design will eventually succumb to technical debt—short-term shortcuts that make future changes prohibitively expensive.

Testing


In our quest to construct high-quality software, testing stands as the most popular and essential quality assurance activity. While other techniques like static analysis, model checking, and code reviews are valuable, testing is often the primary pillar of industry-standard quality assurance.

Test Classifications

Regression Testing

As software evolves, we must ensure that new features don’t inadvertently break existing functionality. This is the purpose of regression testing—the repetition of previously executed test cases. In a modern agile environment, these are often automated within a Continuous Integration (CI) pipeline, running every time code is changed

Black-Box and White-Box

When we design tests, we usually adopt one of two mindsets. Black-box testing treats the system as a “black box” where the internal workings are invisible; tests are derived strictly from the requirements or specification to ensure they don’t overfit the implementation. In contrast, white-box testing requires the tester to be aware of the inner workings of the code, deriving tests directly from the implementation to ensure high code coverage.

The Testing Pyramid: Levels of Execution

A robust testing strategy requires a mix of tests at different levels of abstraction.

These levels include:

  • Unit Testing: The execution of a complete class, routine, or small program in isolation.
  • Component Testing: The execution of a class, package, or larger program element, often still in isolation.
  • Integration Testing: The combined execution of multiple classes or packages to ensure they work correctly in collaboration.
  • System Testing: The execution of the software in its final configuration, including all hardware and external software integrations.

Interactive Tutorials

Two browser-based tutorials let you practice these ideas on live code:

  • Testing Foundations — assertions, equivalence partitions, boundary values, oracle strength, and testing behavior rather than implementation.
  • TDD — Red-Green-Refactor with pytest, katas, and AI-assisted TDD. Builds on Testing Foundations.

Test Quality and Test Design

Before choosing a tool or chasing a coverage number, ask whether the tests are good evidence. The new pages in this chapter separate two questions:

  • Test Quality explains how to evaluate a whole suite: oracle strength, fault-revealing power, coverage limits, mutation testing, flakiness, and maintainability.
  • Writing Good Tests gives a practical recipe for individual tests: behavior-focused names, small fixtures, strong assertions, systematic input selection, deterministic execution, and TDD as a rhythm of small verified steps.

Testability

Quality Attributes


While functionality describes exactly what a software system does, quality attributes describe how well the system performs those functions. Quality attributes measure the overarching “goodness” of an architecture along specific dimensions, encompassing critical properties such as extensibility, availability, security, performance, robustness, interoperability, and testability.

Important quality attributes include:

  • Interoperability: the degree to which two or more systems or components can usefully exchange meaningful information via interfaces in a particular context.

  • Testability: degree to which a system or component can be tested via runtime observation, determining how hard it is to write effective tests for a piece of software.

The Architectural Foundation: “Load-Bearing Walls”

Quality attributes are often described as the load-bearing walls of a software system. Just as the structural integrity of a building depends on walls that cannot be easily moved once construction is finished, early architectural decisions strongly impact the possible qualities of a system. Because quality attributes are typically cross-cutting concerns spread throughout the codebase, they are extremely difficult to “add in later” if they were not considered early in the design process.

Categorizing Quality Attributes

Quality attributes can be broadly divided into two categories based on when they manifest and who they impact:

  • Design-Time Attributes: These include qualities like extensibility, changeability, reusability, and testability. These attributes primarily impact developers and designers, and while the end-user may not see them directly, they determine how quickly and safely the system can evolve.
  • Run-Time Attributes: these include qualities like performance, availability, and scalability. These attributes are experienced directly by the user while the program is executing.

Specifying Quality Requirements

To design a system effectively, quality requirements must be measurable and precise rather than broad or abstract. A high-quality specification requires two parts: a scenario and a metric.

  • The Scenario: This describes the specific conditions or environment to which the system must respond, such as the arrival of a certain type of request or a specific environmental deviation.
  • The Metric: This provides a concrete measure of “goodness”. These can be hard thresholds (e.g., “response time < 1s”) or soft goals (e.g., “minimize effort as much as possible”).

For example, a robust specification for a Mars rover would not just say it should be “robust”, but that it must “function normally and send back all information under extreme weather conditions”.

Trade-offs and Synergies

A fundamental reality of software design is that you cannot always maximize all quality attributes simultaneously; they frequently conflict with one another.

  • Common Conflicts: Enhancing security through encryption often decreases performance due to the extra processing required. Similarly, ensuring high reliability (such as through TCP’s message acknowledgments) can reduce performance compared to faster but unreliable protocols like UDP.
  • Synergies: In some cases, attributes support each other. High performance can improve usability by providing faster response times for interactive systems. Furthermore, testability and changeability often synergize, as modular designs that are easy to change also tend to be easier to isolate for testing.

Interoperability


Interoperability is defined as the degree to which two or more systems or components can usefully exchange meaningful information via interfaces in a particular context.

Motivation

In the modern software landscape, systems are rarely “islands”; they must interact with external services to function effectively

Interoperability is a fundamental business enabler that allows organizations to use existing services rather than reinventing the wheel. By interfacing with external providers, a system can leverage specialized functionality for email delivery, cloud storage, payment processing, analytics, and complex mapping services. Furthermore, interoperability increases the usability of services for the end-user; for instance, a patient can have their electronic medical records (EMR) seamlessly transferred between different hospitals and doctors, providing a level of care that would be impossible with fragmented data.

From a technical perspective, interoperability is the glue that supports cross-platform solutions. It simplifies communication between separately developed systems, such as mobile applications, Internet of Things (IoT) devices, and microservices architectures.

Specifying Interoperability Requirements

To design effectively for interoperability, requirements must be specified using two components: a scenario and a metric.

  • The Scenario: This must describe the specific systems that should collaborate and the types of data they are expected to exchange.
  • The Metric: The most common measure is the percentage of data exchanged correctly.

Syntactic vs Semantic Interoperability

To master interoperability, an engineer must distinguish between its two fundamental dimensions: syntactic and semantic. Syntactic interoperability is the ability to successfully exchange data structures. It relies on common data formats, such as XML, JSON, or YAML, and shared transport protocols, such as HTTP(S). When two systems can parse each other’s data packets and validate them against a schema, they have achieved syntactic interoperability.

However, a major lesson in software architecture is that syntactic interoperability is not enough. Semantic interoperability requires that the exchanged data be interpreted in exactly the same way by all participating systems. Without a shared interpretation, the system will fail even if the data is transmitted flawlessly. For example, if a client system sends a product price as a decimal value formatted perfectly in XML, but assumes the price excludes tax while the receiving server assumes the price includes tax, the resulting discrepancy represents a severe semantic failure. An even more catastrophic example occurred with the Mars Climate Orbiter (1999), where a $327 M spacecraft was lost because one ground-software component computed thruster firing impulses in pound-force-seconds (lbf·s) — US customary units — while the receiving navigation software expected the same impulses in newton-seconds (N·s) — the Système International (SI) unit. The 4.45× discrepancy quietly accumulated across many tiny burns, leaving the orbiter on a trajectory that brought it ~57 km above the Martian surface instead of the planned ~226 km, where it disintegrated.

To achieve true semantic interoperability, engineers must rigorously define the semantics of shared data. This is done by documenting the interface with a semantic view that details the purpose of the actions, expected coordinate systems, units of measurement, side-effects, and error-handling conditions. Furthermore, systems should rely on shared dictionaries and standardized terminologies.

Architectural Tactics and Patterns

When systems must interact but possess incompatible interfaces, the Adapter design pattern is the primary solution. An adapter component acts as a translator, sitting between two systems to convert data formats (syntactic translation) or map different meanings and units (semantic translation). This approach allows the systems to interoperate without requiring changes to their core business logic.

In modern microservices architectures, interoperability is managed through Bounded Contexts. Each service handles its own data model for an entity, and interfaces are kept minimal—often sharing only a unique identifier like a User ID—to separate concerns and reduce the complexity of interactions.

Trade-offs

Interoperability often conflicts with changeability. Standardized interfaces are inherently difficult to update because a change to the interface cannot be localized to a single system; it requires all participating systems to update their implementations simultaneously.

The GDS case study highlights this dilemma. Because the GDS interface is highly standardized, it struggled to adapt to the business model of Southwest Airlines, which does not use traditional seat assignments. Updating the GDS standard to support Southwest would have required every booking system and airline in the world to change their software, creating a massive implementation hurdle.

“Practical Interoperability”

In a real-world setting, a design for interoperability is evaluated based on its likelihood of adoption, which involves two conflicting measures:

  1. Implementation Effort: The more complex an interface is, the less likely it is to be adopted due to the high cost of implementation across all systems.
  2. Variability: An interface that supports a wide variety of use cases and potential extensions is more likely to be adopted.

Successful interoperable design requires finding the “sweet spot” where the interface provides enough variability to be useful while remaining simple enough to minimize adoption costs.


References

  1. (Amna and Poels 2022): Anis R. Amna and Geert Poels (2022) “A Systematic Literature Mapping of User Story Research,” IEEE Access, 10, pp. 52230–52260.
  2. (Amna and Poels 2022): Asma Rafiq Amna and Geert Poels (2022) “Ambiguity in user stories: A systematic literature review,” Information and Software Technology, 145, p. 106824.
  3. (Bass et al. 2012): Len Bass, Paul Clements, and Rick Kazman (2012) Software Architecture in Practice. 3rd ed. Addison-Wesley.
  4. (Beck and Andres 2004): Kent Beck and Cynthia Andres (2004) Extreme Programming Explained: Embrace Change. 2nd ed. Boston, MA: Addison-Wesley Professional.
  5. (Buschmann et al. 1996): Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad, and Michael Stal (1996) Pattern-Oriented Software Architecture: A System of Patterns. John Wiley & Sons.
  6. (Cohn 2004): Mike Cohn (2004) User Stories Applied: For Agile Software Development. Addison-Wesley Professional.
  7. (Dalpiaz and Sturm 2020): Fabiano Dalpiaz and Arnon Sturm (2020) “Conceptualizing Requirements Using User Stories and Use Cases: A Controlled Experiment,” International Working Conference on Requirements Engineering: Foundation for Software Quality (REFSQ). Springer, pp. 221–238.
  8. (Foote and Yoder 1997): Brian Foote and Joseph Yoder (1997) “Big Ball of Mud.” Pattern Languages of Programs Conference (PLoP ’97).
  9. (Freeman and Robson 2020): Eric Freeman and Elisabeth Robson (2020) Head First Design Patterns. 2nd ed. O’Reilly Media.
  10. (Gamma et al. 1995): Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (1995) Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
  11. (Hallmann 2020): Daniel Hallmann (2020) “‘I Don’t Understand!’: Toward a Model to Evaluate the Role of User Story Quality,” International Conference on Agile Software Development (XP). Springer (LNBIP), pp. 103–112.
  12. (Kassab 2015): Mohamad Kassab (2015) “The Changing Landscape of Requirements Engineering Practices over the Past Decade,” IEEE Fifth International Workshop on Empirical Requirements Engineering (EmpiRE). IEEE, pp. 1–8.
  13. (Lauesen and Kuhail 2022): Soren Lauesen and Mohammad A. Kuhail (2022) “User Story Quality in Practice: A Case Study,” Software, 1, pp. 223–241.
  14. (Lucassen et al. 2016): Garm Lucassen, Fabiano Dalpiaz, Jan Martijn E. M. van der Werf, and Sjaak Brinkkemper (2016) “The Use and Effectiveness of User Stories in Practice,” International Working Conference on Requirements Engineering: Foundation for Software Quality (REFSQ). Springer, pp. 205–222.
  15. (Lucassen et al. 2016): Gijs Lucassen, Fabiano Dalpiaz, Jan Martijn van der Werf, and Sjaak Brinkkemper (2016) “Improving agile requirements: the Quality User Story framework and tool,” Requirements Engineering, 21(3), pp. 383–403.
  16. (Molenaar and Dalpiaz 2025): Sabine Molenaar and Fabiano Dalpiaz (2025) “Improving the Writing Quality of User Stories: A Canonical Action Research Study,” International Working Conference on Requirements Engineering: Foundation for Software Quality (REFSQ). Springer.
  17. (Ousterhout 2021): John K. Ousterhout (2021) A Philosophy of Software Design. 2nd ed. Yaknyam Press.
  18. (Parnas 1972): David L. Parnas (1972) “On the Criteria To Be Used in Decomposing Systems into Modules,” Communications of the ACM, 15(12), pp. 1053–1058.
  19. (Quattrocchi et al. 2025): Giovanni Quattrocchi, Liliana Pasquale, Paola Spoletini, and Luciano Baresi (2025) “Can LLMs Generate User Stories and Assess Their Quality?,” IEEE Transactions on Software Engineering.
  20. (Rittel and Webber 1973): Horst Wilhelm Johannes Rittel and Melvin M. Webber (1973) “Dilemmas in a General Theory of Planning,” Policy Sciences, 4(2), pp. 155–169.
  21. (Santos et al. 2025): Reine Santos, Gabriel Freitas, Igor Steinmacher, Tayana Conte, Ana Carolina Oran, and Bruno Gadelha (2025) “User Stories: Does ChatGPT Do It Better?,” International Conference on Enterprise Information Systems (ICEIS). SciTePress.
  22. (Schwaber and Sutherland 2020): Ken Schwaber and Jeff Sutherland (2020) “The Scrum Guide.”
  23. (Scott et al. 2021): Ezequiel Scott, Tanel Tõemets, and Dietmar Pfahl (2021) “An Empirical Study of User Story Quality and Its Impact on Open Source Project Performance,” International Conference on Software Quality, Reliability and Security (SWQD). Springer (LNBIP), pp. 119–138.
  24. (Sharma and Tripathi 2025): Amol Sharma and Anil Kumar Tripathi (2025) “Evaluating user story quality with LLMs: a comparative study,” Journal of Intelligent Information Systems, 63, pp. 1423–1451.
  25. (Wake 2003): Bill Wake (2003) “INVEST in Good Stories: The Series.”
  26. (Wang et al. 2014): Xiaofeng Wang, Lianging Zhao, Yong Wang, and Jian Sun (2014) “The Role of Requirements Engineering Practices in Agile Development: An Empirical Study,” Asia Pacific Requirements Engineering Symposium (APRES). Springer (CCIS), pp. 195–209.