Low-Level Design: Class Relationships and Design Patterns in Go and Python

Tisan Das · · 57 min read

Contents

    Low-level design rests on two skills: telling how two classes are related, and recognizing design patterns, which are those same relationships arranged on purpose to solve a problem that keeps coming back. This post consists of six parts. Part 1 and Part 2 cover relationships, with 100 practice questions. Parts 3 to 5 (creational, structural, behavioral) cover the patterns, each with a class diagram, Go code (Python where the idiom differs), the APIs you already use that follow it, and the traps. Part 6 is a field guide for when you have a real problem and need the right pattern.

    Each part ends with a quiz. Answers are saved in this browser, so you can revisit after few days and re-run them cold. The Go samples need Go 1.21 or later (1.23 for iter and unique); the Python samples need 3.10 or later.

    Disclaimer. This post is drafted with assistance from large language models (Claude Opus 5.5 and DeepSeek V4.1 Flash) based on conversations exploring low-level design and design patterns. All content has been reviewed, edited, and verified by a human author.

    Part 1: Association, aggregation or composition?

    Most low-level design problems, from parking lots to movie booking, keep asking one question: how are these two classes related? Association, aggregation and composition look identical in code. What separates them is ownership and lifetime. Get that call right and the rest of the design follows: who constructs what, what a delete cascades to, who may close a connection, and what a lock actually protects.

    The two questions that separate them

    In code, all three are a field in class A that refers to class B. Two questions about ownership and lifetime tell them apart:

    • If I delete A, what happens to B?
    • Can B belong to more than one A at the same time?
      Whole–part? B survives deleting A? B shareable?
    Association No, they’re peers Yes Yes, often many-to-many
    Aggregation Yes Yes Yes, or it can move between wholes
    Composition Yes No, it dies with A No, exactly one owner

    Three examples from common interview problems:

    • Association. A parking lot’s Vehicle and ParkingSpot: the car existed before it parked and drives away after.
    • Aggregation. Splitwise’s Group and User: delete the trip group and your friends keep their accounts.
    • Composition. Movie booking’s Screen and Seat: seat A9 means nothing without its screen.

    The full spectrum and its notation

    Those three sit in the middle of a wider range of relationships, ordered by how tightly the two classes are coupled:

    The relationship spectrum from weaker to stronger coupling: dependency (uses-a, dashed arrow), association (knows-a, plain line), aggregation (has-a, hollow diamond), composition (owns-a, filled diamond), and at the strong end inheritance (is-a, hollow triangle) with realization (implements, hollow triangle on a dashed line) at the same level The middle three are the ones most designs argue about. Realization, implementing an interface, sits level with inheritance: the same hollow triangle, on a dashed line.

    How to read the notation:

    • The diamond always sits on the whole’s end. Hollow ◇ is aggregation, filled ◆ is composition. In plain text you’ll see them typed as A <>-- B and A <#>-- B.
    • A plain solid line is an association. Multiplicities such as 1 and 0..* go on its ends.
    • A dashed open arrow is a dependency. A uses B somewhere in its code but doesn’t keep it.
    • A hollow triangle points at a parent type. A solid line means inheritance from a class; a dashed line means realization of an interface.
    • An association class hangs off the middle of an association line by a dashed line.

    Spotting them in code and schemas

    In code, the tell is who calls B’s constructor, and whether B is handed in or created inside.

    Python · the same field, three relationships
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    # Association: peers created independently, linked by reference
    class Teacher:
        def __init__(self, name: str):
            self.name = name
            self.students: list["Student"] = []
    
        def enroll(self, s: "Student"):
            self.students.append(s)
            s.teachers.append(self)          # often bidirectional
    
    # Aggregation: the whole holds parts that were created elsewhere
    class Library:
        def __init__(self):
            self.books: list[Book] = []
    
        def add(self, book: Book):           # passed in: the caller created it
            self.books.append(book)
    
    # Composition: the whole creates its parts and never hands out ownership
    class House:
        def __init__(self, room_specs):
            self._rooms = [Room(s) for s in room_specs]   # created inside
    

    In a garbage-collected language nothing is destroyed when A goes away; B is freed only once nothing points to it. So composition is a design rule the runtime won’t enforce: A alone creates and manages B, and no other object keeps a reference to B after A is gone. Other code may borrow B during a call, but it shouldn’t store it.

    The database view makes the difference concrete, and interviewers do probe it:

    Relationship Schema shape On delete of A
    Association Join table, such as teacher_student Delete only the join rows
    Aggregation Nullable foreign key on B, such as book.library_id ON DELETE SET NULL
    Composition NOT NULL foreign key on B, such as room.house_id ON DELETE CASCADE
    Association class Join table with its own columns, such as enrollment(student_id, course_id, grade) Delete the link rows, and their data goes with them; or RESTRICT the delete if that data must be kept

    In Go. Composition is a struct that creates and holds its parts, often as value fields or unexported pointers. Aggregation is a struct holding pointers injected through its constructor. Struct embedding isn’t inheritance: it’s composition with method promotion. And the advice “favor composition over inheritance” uses the word loosely, meaning “hold an object instead of subclassing”, which covers aggregation and association too.

    Association versus aggregation is the blurriest line. UML leaves aggregation’s meaning loose, and Martin Fowler’s UML Distilled recommends essentially ignoring it. The distinction that matters most is composition versus everything else, because it decides cascading deletes, who constructs what, and whether a part can be shared.

    Dependency or association?

    These two get confused more than any other pair. One question settles it: does A still hold a reference to B after the method returns?

    • Yes: association. B is part of A’s state, stored in a field.
    • No: dependency. A touches B only while one method runs, through a parameter, a local variable, a return value or a static call.

    Put another way, an association is a link between objects, and a dependency is a link between pieces of code. In lot.park(car), the lot reaches the car only through the parameter, then builds a Ticket that stores it in a field. So ParkingLot → Vehicle is a dependency and Ticket → Vehicle is an association: the same Vehicle in two relationships, decided by who keeps the reference.

    The snapshot test. Freeze the program at a moment when no method is running and look at the object graph. Every arrow you can still see is an association. Dependencies never show up in a snapshot, because they exist only while code is running.

    Python · dependency versus association
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    # Dependency: A uses B, then forgets it
    class ParkingLot:
        def park(self, vehicle: Vehicle) -> Ticket:     # parameter
            spot = self._find_spot(vehicle.size)
            return Ticket(spot, vehicle)                # creates and returns, keeps nothing
    
    # Association: A keeps B
    class Ticket:
        def __init__(self, spot: ParkingSpot, vehicle: Vehicle):
            self.spot = spot                            # stored
            self.vehicle = vehicle                      # stored
    

    On a class diagram, fields draw solid lines and method signatures draw dashed ones.

    Class diagram of Order: its fields customer and items draw solid lines, an association to Customer and a composition with LineItem; its methods total(tax: TaxCalculator), notify(mailer: Mailer) and invoice() returning Invoice draw dashed dependency arrows Trace each line back to the member it comes from. Fields, which Order keeps, draw solid lines; types that only appear in a method signature draw dashed arrows.

      Dependency Association
    Lives in Parameters, locals, return types, static calls Fields (instance state)
    Link lasts One method call As long as A holds the field
    UML Dashed arrow Solid line
    Database Nothing in the schema Foreign key or join table
    Coupling Weaker; prefer it when you can Stronger

    The gray areas

    • Injected services. OrderService.__init__(self, repo) that stores self.repo is an association by the strict test. Many teams still draw injected collaborators as dashed dependencies to keep only domain relationships solid. Either is defensible if you state your convention; this post uses the strict test.
    • Storing an ID. Order.customer_id is still an association at the domain level. The ID is just how the link is stored.
    • Create and return. A factory that builds a Car and returns it depends on Car (UML even has a «create» dependency) but keeps nothing.
    • The same pair can be either. Report.print(printer) is a dependency; a Report holding self.printer is an association. The code decides, not the class names.

    Two design signals come out of this. If you pass the same object into most of a class’s methods, it’s probably state: make it a field. If a field is used by only one method, it probably isn’t state: make it a parameter. Less state means fewer lifecycle and concurrency problems, so default to dependency and promote to association only when A genuinely needs to remember B.

    Sometimes the relationship itself carries data. A grade belongs neither to the student nor to the course, but to the pairing. The quantity of a product belongs neither to the cart nor to the product. When the link has attributes, promote it to a class of its own, an association class: Enrollment, CartItem, Membership.

    • Parking lot: the spot–vehicle link carries an entry time and a fee, so it becomes Ticket.
    • Movie booking: Screen ◆ Seat is composition, but “seat A9 for the 7 pm show” is a different thing: ShowSeat, the association class between Show and Seat that holds booking status and price. It’s also what gets locked when two people try to book the same seat.

    In a database, an association class is a join table with its own columns. One subtlety: strictly, a UML association class allows one link per pair. If the same pair can repeat, such as the same song twice in one playlist, model the link as a full class, or mark the association ends {nonunique}.

    The decision tree

    Everything above folds into five questions, asked in order. Stop at the first one that gives an answer. The order is for classification, not preference: inheritance comes first because it’s the easiest to rule in or out.

    The decision tree: 1. kind of or implements? (extends gives inheritance, implements gives realization); 2. is the link stored? (no gives dependency); 3. does the link carry data? (yes gives association class); 4. is it whole–part? (no gives association); 5. can the part outlive the whole or be shared? (yes gives aggregation, no gives composition) Highlighted outcomes are the stored-link family; the rest are type relationships or transient use.

    What to look for at each step

    Step Signals in code and schemas
    1. Kind of, or implements? extends or a subclass, including an abstract class with concrete methods → inheritance. implements, a Python Protocol, a Go method set, or Rust impl Trait for → realization.
    2. Stored? A field of any kind, including injected services, IDs, weak references and borrowed references → stored. A parameter, local, return type, thrown exception, static or module-level call, or something created and returned → dependency.
    3. Link has data? Data that belongs to neither side alone. A join table with extra columns.
    4. Whole–part? Is A made of B? Possessive English isn’t enough: a doctor “has” patients but isn’t made of them.
    5. Outlive or shared? Composition: created inside, private, a value field, unique_ptr, Box, NOT NULL plus CASCADE. Aggregation: passed in, the same instance in several wholes, shared_ptr, a nullable foreign key with SET NULL.

    The conventions used throughout this post, so that every question has one defensible answer:

    • Injected services stored in a field count as associations (the strict test).
    • Storing an ID instead of an object is still an association.
    • Value fields, unique_ptr and Box are composition; Go struct embedding by value is composition, not inheritance.
    • Composition fixes when parts must die, not when they may: deleting the whole deletes its remaining parts, but the whole can delete a part earlier, such as removing one line item from an order. That’s still composition; UML allows it.
    • Domain rules beat class names. Car → Engine is composition at a carmaker and aggregation at an engine-refurbishing shop.

    What the decision buys you

    Classifying relationships is the vocabulary. What you use it for is the decision every design comes down to: who creates each object, who is allowed to destroy it, and who else can reach it. Once you’ve chosen the relationship, a set of code decisions follows:

    A → B is… Who creates B Who cleans B up Database Locking
    Dependency The caller, per call The caller Nothing Nothing held, nothing to guard
    Association Someone else; injected B’s real owner, never A Foreign key on the “many” side (often A, such as ticket.vehicle_id) or a join table; no cascade B is shared, so it must be safe on its own
    Aggregation Outside, then added to A Outside; B can outlive A Nullable foreign key on B pointing to A, SET NULL A’s lock doesn’t cover B: other wholes can reach it
    Composition A, in its constructor A, when it closes or is deleted NOT NULL foreign key on B pointing to A, CASCADE A’s lock can guard B; nobody else holds it
    Association class Whoever creates the link Removed with the link Join table with foreign keys to both sides, plus its own columns Often the row you lock, such as ShowSeat

    Two rules from that table come up constantly:

    • Only close what you composed. If a struct receives a *sql.DB or a *grpc.ClientConn through its constructor, that’s an association, and it must never call Close() on it. The owner does.
    • A lock on the whole protects only its composed parts. If a part can be shared, another whole can reach it without taking your lock, so the part needs its own protection.

    Where it fits in a design

    Relationships are step 3 of a repeatable low-level design process:

    1. Clarify requirements. Four to six use cases as verbs (“park vehicle”, “exit and pay”). Ask only questions that change the design.
    2. Identify entities. The nouns in those use cases, minus attributes posing as classes: a vehicle’s color is a field.
    3. Map relationships. Run the decision tree on each pair, and promote links that carry data to association classes.
    4. Assign behavior. Give each verb to the class that already has the data it needs.
    5. Model state. Anything with a lifecycle gets an enum and its legal transitions, such as ShowSeat: AVAILABLE → LOCKED → BOOKED.
    6. Apply patterns where things vary. Pricing rules suggest Strategy; spot or vehicle types suggest Factory.
    7. Handle concurrency. Two cars, one spot: pick a per-entity lock, an optimistic version check or a unique constraint, and make payment idempotent.
    8. Code and walk through. Interfaces, core classes and one end-to-end method such as park(), then trace a use case through them.

    Step 3 for a parking lot

    Parking lot class diagram: ParkingLot composes Floor, which composes ParkingSpot; Ticket links ParkingSpot and Vehicle; Car and Truck inherit from Vehicle; ParkingLot is associated with PricingStrategy, which Hourly and Flat rate implement Most of the relationship types in one picture. Ticket, in orange, is the association class: the spot–vehicle link promoted to a class of its own because it carries data.

    • ParkingLot ◆ Floor ◆ ParkingSpot is composition all the way down. The lot builds floors and spots from configuration, and spot “F2-17” means nothing without them.
    • Ticket is the spot–vehicle link promoted to an association class: it carries an entry time and a fee, and the car exists before and after it parks.
    • ParkingLot — PricingStrategy is a plain association: injected and swappable, but a lot isn’t made of its pricing rules. It’s also the Strategy pattern.
    • Car and Truck inherit from Vehicle. If they differ only in size, a VehicleType enum is simpler; subclass only when behavior differs.

    The usual traps: labelling everything composition because “the lot has spots”, spending twenty minutes on aggregation versus association, and running out of time before state and concurrency, which is where strong answers stand out.

    A real-world call: who owns the instance?

    Relationship thinking settles real infrastructure questions too. Say a service keeps a pool of pre-warmed virtual machines in an AWS Auto Scaling group, hands one out per customer request, applies the customer’s configuration, and terminates the machine when the customer is done. Should the instance be detached from the group once it’s assigned?

    The question is really which object should compose the instance once it’s assigned:

    • If you don’t detach, the group still owns it. A scale-in event or a failed health check can terminate a customer’s live machine. Instance scale-in protection stops the first, but not health-check replacement.
    • If you detach, ownership moves to the site. That’s still composition, because a part can leave its whole before the whole is deleted. The duties move with ownership: the service must now terminate the instance when the site is discarded, and replace it if it fails.
    • Detaching can also refill the pool. Detach without decrementing the group’s desired capacity and the group launches a replacement.

    Check your understanding

    1. 1

      Class A has a field of type B. Which facts decide whether that’s an association, an aggregation or a composition?

      1. The field’s type, and whether it’s a single reference or a list
      2. What happens to B when A is deleted, and whether B can belong to more than one A
      3. Whether the field is public or private
      4. Whether A calls methods on B
      Show answer

      Answer What happens to B when A is deleted, and whether B can belong to more than one A

      The code looks the same in all three cases. Ownership and lifetime separate them: if B dies with A and has exactly one owner, it’s composition; if it’s a part that survives or can be shared, aggregation; if the two are peers, association. Visibility and collection types are hints at most.

    2. 2

      You freeze a running program at a moment when no method is executing, and look at the object graph. Which relationships can you still see?

      1. Every relationship on the class diagram
      2. Only dependencies
      3. Associations, including aggregations and compositions, but no dependencies
      4. Only compositions
      Show answer

      Answer Associations, including aggregations and compositions, but no dependencies

      This is the snapshot test. A dependency exists only while code runs, through a parameter, a local or a return value, so it disappears with the stack frame. Anything stored in a field is still there. Inheritance and realization aren’t links between objects at all; they’re relationships between types.

    3. 3

      ParkingLot.park(vehicle) finds a spot, creates Ticket(spot, vehicle) and returns it. The lot stores nothing about the vehicle; the ticket keeps it in a field. What are ParkingLot → Vehicle and Ticket → Vehicle?

      1. Both are associations
      2. Association, then dependency
      3. Both are dependencies
      4. Dependency, then association
      Show answer

      Answer Dependency, then association

      Same Vehicle, two relationships, decided by who keeps the reference. The lot reaches the car only through a parameter for the length of one call, so that’s a dependency. The ticket stores it in a field that outlives the call, so that’s an association.

    4. 4

      A class receives a Clock as a parameter of one method and never stores it. At which step does the decision tree stop?

      1. Step 1, “kind of, or implements?”
      2. Step 2, “is the link stored?”, with dependency
      3. Step 4, “is it whole–part?”, with association
      4. Step 5, “outlive or shared?”, with aggregation
      Show answer

      Answer Step 2, “is the link stored?”, with dependency

      The class isn’t a kind of Clock and doesn’t implement it, so step 1 says neither. Step 2 asks whether the link still exists after the method returns. It doesn’t, so the answer is dependency, and the questions about data, whole–part and lifetime never come up.

    5. 5

      A student’s grade in a course belongs to neither the student nor the course. Where should it live?

      1. In a field on Student, keyed by course
      2. In a field on Course, keyed by student
      3. In a Grade class composed by Course
      4. In an association class such as Enrollment, which links one student to one course
      Show answer

      Answer In an association class such as Enrollment, which links one student to one course

      Data that belongs to the pairing, not to either side, is the signal for an association class. Enrollment carries the grade, and in a database it’s a join table with its own columns. A map on either side works, but it hides the link and duplicates it if both sides need it.

    6. 6

      A playlist can contain the same song more than once. Why is a plain UML association class between Playlist and Song not quite right?

      1. A UML association class allows only one link per pair, so a repeated song needs a full class such as PlaylistEntry, or ends marked {nonunique}
      2. Association classes can’t hold an ordering
      3. Songs are composed by albums, so they can’t be linked to playlists
      4. Playlists aggregate songs, so no link class is needed
      Show answer

      Answer A UML association class allows only one link per pair, so a repeated song needs a full class such as PlaylistEntry, or ends marked {nonunique}

      Strictly, an association class instance is identified by the pair it links, so the same pair can’t appear twice. If repeats are allowed, make the link a class with its own identity, or mark the association ends {nonunique}.

    7. 7

      Which schema shape matches composition, such as a room row that belongs to a house?

      1. A join table such as house_room, deleting only the join rows
      2. A nullable room.house_id with ON DELETE SET NULL
      3. A NOT NULL foreign key room.house_id with ON DELETE CASCADE
      4. Nothing in the schema
      Show answer

      Answer A NOT NULL foreign key room.house_id with ON DELETE CASCADE

      NOT NULL says a room can’t exist without a house, and CASCADE deletes the rooms with it. A nullable foreign key with SET NULL is aggregation: the part survives, unattached. A join table is a plain association. A dependency leaves no trace in the schema.

    8. 8

      A Repo struct receives a *sql.DB pool through its constructor and stores it. Should Repo.Close() call db.Close()?

      1. Yes, because it holds the reference
      2. Only if it’s the last Repo still using the pool
      3. No. It’s an association, and whoever created the pool closes it.
      4. Yes, but only in tests
      Show answer

      Answer No. It’s an association, and whoever created the pool closes it.

      Only close what you composed. The repo didn’t create the pool and other code may share it, so closing it would break every other user. The owner, usually main, closes it once at shutdown.

    9. 9

      An Order guards its state with a mutex. It composes its LineItems and holds a reference to a shared Customer. What does taking the order’s lock protect?

      1. The order and its line items, but not the customer
      2. Everything reachable from the order
      3. The order and the customer, but not the line items
      4. Only the order’s own scalar fields
      Show answer

      Answer The order and its line items, but not the customer

      A lock on the whole protects only what nobody else can reach. The line items are composed, so every path to them goes through the order and its lock. The customer is shared: other orders, and other code, can reach it without taking this lock, so it needs its own protection.

    10. 10

      In Go, type Car struct { Engine } embeds an Engine struct by value. What relationship is that?

      1. {“Inheritance” => “a Car is an Engine”}
      2. Realization of the Engine type
      3. Composition with method promotion
      4. A dependency, because embedding is resolved at compile time
      Show answer

      Answer Composition with method promotion

      Embedding by value puts a whole Engine inside every Car, created with it and gone with it, so it’s composition. Go promotes the engine’s methods so you can call them on the car, but a Car is never usable where an Engine is expected, so it isn’t inheritance.

    11. 11

      In the parking lot model, why is ParkingLot — PricingStrategy a plain association rather than a composition?

      1. The strategy is injected and can be swapped, and a lot isn’t made of its pricing rules
      2. Interfaces can never be composed
      3. The lot only uses pricing inside exit(), so it’s really a dependency
      4. A pricing strategy is a kind of parking lot
      Show answer

      Answer The strategy is injected and can be swapped, and a lot isn’t made of its pricing rules

      The lot stores the strategy, so it’s at least an association. The whole–part question fails: pricing rules aren’t a part of the lot the way floors are, and the same strategy object could serve several lots. Nothing stops a field of interface type from being composed, if the whole creates and owns it.

    12. 12

      Why do many designers stop arguing about association versus aggregation?

      1. UML 2 removed aggregation from the standard
      2. UML defines aggregation loosely, and composition versus everything else is the line that changes the code
      3. Databases can’t represent aggregation
      4. Aggregation and composition mean the same thing
      Show answer

      Answer UML defines aggregation loosely, and composition versus everything else is the line that changes the code

      UML gives aggregation almost no semantics beyond association, and Fowler’s UML Distilled recommends ignoring it. Composition is different: it decides cascading deletes, who constructs the part, and whether the part can be shared. Pick one, state your lifecycle reasoning, and move on.

    13. 13

      A service hands customers instances from an Auto Scaling group and doesn’t detach them, but turns on instance scale-in protection. What can still terminate a customer’s live machine?

      1. Nothing; scale-in protection covers every termination
      2. Only a manual terminate-instances call by the customer
      3. The group replacing the instance after it fails a health check
      4. The group detaching it automatically after an hour
      Show answer

      Answer The group replacing the instance after it fails a health check

      Scale-in protection only stops the group choosing the instance during scale-in. An instance that fails its health check is still replaced. While the group composes the instance, the group decides when it dies; detaching moves that ownership, and the duty to terminate and replace it, to the site.

    Part 2: 100 questions on class relationships

    Running the decision tree quickly on code you’ve never seen takes practice. These 100 questions cover code in Python, Go, Java, TypeScript, C++ and Rust, SQL schemas, UML and real systems. Pairs that differ by one fact recur, so you practice the deciding detail rather than the class names. Answers follow the conventions from Part 1; where a second answer is defensible, it’s marked as also accepted. Walk the tree mode asks the five questions one at a time and shows which step went wrong.

    What is the relationship?

    1. Dependency uses it, keeps nothing
    2. Association stored link between peers
    3. Association class the link has its own data
    4. Aggregation part survives or is shared
    5. Composition exclusive, dies with the whole
    6. Inheritance is a kind of
    7. Realization implements a contract
    1. 1Chessboard → SquareCode · Python · Chess

      A chess engine models its board like this.

      Python
      1
      2
      3
      4
      5
      6
      7
      
      class Chessboard:
          def __init__(self):
              self._squares = [[Square(r, c) for c in range(8)]
                               for r in range(8)]
      
          def square_at(self, r, c) -> "Square":
              return self._squares[r][c]
      
      Show answer

      Answer Composition

      Deciding clue
      The board builds its own squares in __init__, keeps them private, and a square like “e4” means nothing without a board.
      Why
      Nothing outside the board ever creates a Square, the board never accepts squares from a caller, and the squares go when the board goes. square_at() lends a square out for reading, which doesn’t transfer ownership.
      Tempting wrong turn
      Aggregation is tempting because square_at() exposes the squares. Exposing a part for reading isn’t the same as letting it live independently or be shared by another board.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    2. 2Playlist → SongScenario · Music streaming

      Songs live in the catalog. A playlist is an ordered collection of songs; a song can appear in thousands of playlists, and deleting a playlist never deletes a song.

      Show answer

      Answer Aggregation

      Deciding clue
      A collection whose members exist independently and are shared.
      Why
      A playlist is meaningfully “made of” songs (whole–part), but the songs outlive it and are shared across playlists. Classic aggregation.
      Tempting wrong turn
      Composition is tempting because a playlist is nothing but its songs. The test isn’t how much of the whole the parts make up, it’s whether the parts survive.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    3. 3Employee → Employee (manager)Code · Python · HR

      An org chart stores each employee’s manager.

      Python
      1
      2
      3
      4
      
      class Employee:
          def __init__(self, name: str, manager: "Employee | None"):
              self.name = name
              self.manager = manager
      
      Show answer

      Answer Association

      Deciding clue
      A stored reference to an independent peer; neither is part of the other.
      Why
      Same class on both ends makes it a reflexive (self) association. The link is held in a field, so it isn’t a dependency. A manager isn’t made of their reports, and firing a manager doesn’t delete anyone, so it isn’t whole–part.
      Tempting wrong turn
      Aggregation is tempting because a manager “has” a team. In English “has” is cheap; ask whether the team is structurally a part of the manager object. It isn’t.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    4. 4Document → CommentScenario · Google Docs

      Each comment is anchored to a range of text in one document. Deleting the document deletes all of its comments, and there’s no way to move a comment to a different document.

      Show answer

      Answer Composition

      Deciding clue
      Comments die with the document and can’t move to another one.
      Why
      It’s whole–part (a document’s comment thread is part of the document), the part can’t outlive the whole, and it can’t be shared. Both lifecycle questions point to composition.
      Tempting wrong turn
      Association is tempting because comments have their own authors and timestamps. Having attributes doesn’t make something independent; its lifetime does.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    5. 5FantasyTeam → PlayerCode · Python · Fantasy sports

      In a fantasy league, the same real-world player can be drafted onto many different users’ teams.

      Python
      1
      2
      3
      4
      5
      6
      7
      
      class FantasyTeam:
          def __init__(self, owner: User):
              self.owner = owner
              self.roster: list[Player] = []
      
          def draft(self, player: Player):
              self.roster.append(player)  # from the league's player pool
      
      Show answer

      Answer Aggregation

      Also accepted Association. The aggregation/association line is blurry for membership collections. Aggregation is preferred because a fantasy team is defined by its roster.

      Deciding clue
      A roster is a whole made of players, but each Player comes from a shared pool and appears on many teams.
      Why
      Whole–part: a team is its roster. The part is created elsewhere (the league pool), passed in, survives the team being deleted, and is shared across teams. That’s aggregation.
      Tempting wrong turn
      Composition is tempting because the team “has” its roster. But deleting a fantasy team obviously doesn’t delete the real player.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    6. 6Ride → DriverScenario · Ride hailing

      A Ride records which driver accepted it. Driver accounts exist long before and after any single ride, and one driver completes thousands of rides.

      Show answer

      Answer Association

      Deciding clue
      The ride keeps a lasting reference, but a driver is not a part of a ride.
      Why
      Held after the method returns: yes (the ride stores its driver). Whole–part: no. That leaves association.
      Tempting wrong turn
      Aggregation is tempting because a ride “has” a driver. A ride isn’t assembled from drivers; the driver is a participant, not a component.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    7. 7Server → sync.MutexCode · Go · Networking

      A TCP server guards its connection map with a mutex.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      type Server struct {
          mu    sync.Mutex
          conns map[string]*Conn
      }
      
      func (s *Server) add(id string, c *Conn) {
          s.mu.Lock()
          defer s.mu.Unlock()
          s.conns[id] = c
      }
      
      Show answer

      Answer Composition

      Deciding clue
      mu is a value field, not a pointer, so the mutex is physically laid out inside the Server’s memory.
      Why
      A value field is created with the struct, destroyed with it, and can’t be shared. Copying a Server would copy the mutex, which go vet flags as a bug. That is composition in its purest form: the part is literally inside the whole.
      Tempting wrong turn
      Dependency is tempting because the mutex is “just used” for locking. But it’s stored as state: the strict test says it is still held after every method returns, so it isn’t a dependency.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    8. 8Target group → EC2 instanceScenario · AWS

      An ALB target group lists the instances that receive traffic. Deleting the target group doesn’t terminate any instance, and one instance can be registered in several target groups.

      Show answer

      Answer Aggregation

      Also accepted Association. Defensible if you see the target group as a routing rule that references instances rather than a collection made of them. Most designers read “group” as a whole made of members, hence aggregation.

      Deciding clue
      A group made of members that exist independently and can belong to several groups.
      Why
      The target group is defined by its membership (whole–part), yet its members outlive it and are shared. Membership collections like this are the canonical aggregation.
      Tempting wrong turn
      Composition is tempting because the target group “contains” instances. But terminating the group never terminates the instance.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    9. 9Scheduler → ClockCode · Go · Job scheduling

      A scheduler decides which jobs are due.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      type Scheduler struct {
          jobs []Job
      }
      
      func (s *Scheduler) Due(c Clock) []Job {
          now := c.Now()
          var due []Job
          for _, j := range s.jobs {
              if !j.NextRun.After(now) {
                  due = append(due, j)
              }
          }
          return due
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      Clock appears only as a parameter; the struct has no Clock field.
      Why
      After Due() returns, the scheduler holds no reference to the clock. The link exists only for the duration of one call.
      Tempting wrong turn
      Association is tempting because the scheduler clearly needs a clock to function. Needing something isn’t the same as keeping it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    10. 10Order → OrderLineSchema · SQL · E-commerce

      What relationship does this schema encode?

      SQL
      1
      2
      3
      4
      5
      6
      7
      8
      
      CREATE TABLE order_lines (
        order_id  BIGINT NOT NULL
                  REFERENCES orders(id) ON DELETE CASCADE,
        line_no   INT    NOT NULL,
        sku       TEXT   NOT NULL,
        qty       INT    NOT NULL,
        PRIMARY KEY (order_id, line_no)
      );
      
      Show answer

      Answer Composition

      Deciding clue
      The parent’s id is part of the child’s primary key, the FK is NOT NULL, and deletes cascade.
      Why
      A line’s identity literally includes its order (“order 42, line 3”). NOT NULL rules out orphan lines, and ON DELETE CASCADE means lines die with their order. This is the textbook relational shape of composition.
      Tempting wrong turn
      Association class is tempting because order_lines carries extra columns. But it hangs off one parent, not between two independent entities, so it’s a part, not a link.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    11. 11Department → EmployeeCode · Python · HR

      When a department is dissolved, its employees are moved to other departments.

      Python
      1
      2
      3
      4
      5
      6
      7
      
      class Department:
          def __init__(self, name: str, employees: list[Employee]):
              self.name = name
              self.employees = employees
      
      hr  = Department("HR", [alice, bob])
      eng = Department("Platform", [carol])
      
      Show answer

      Answer Aggregation

      Also accepted Association. Accepted: membership collections sit on the blurry aggregation/association line. Aggregation is preferred because the department is defined by its members.

      Deciding clue
      Employees are created outside, passed in, and survive the department.
      Why
      A department is made up of its people (whole–part), but employees outlive it and can move between departments. Passed-in parts that survive the whole: aggregation.
      Tempting wrong turn
      Association is tempting, and the line between the two is genuinely blurry. The reason to prefer aggregation here is that the department is defined by its members, not merely acquainted with them.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    12. 12OrderPrinter → FormatterRefactor · Receipts

      OrderPrinter has a field self.formatter, but only print_receipt() uses it. You refactor to print_receipt(self, order, formatter) and delete the field. What is the relationship after the refactor?

      Show answer

      Answer Dependency

      Deciding clue
      The field is gone; the formatter now arrives as a parameter.
      Why
      Moving a single-use field into a parameter turns an association into a dependency, which means less state, fewer lifecycle questions, and looser coupling. This is a common and healthy refactor.
      Tempting wrong turn
      Association is the before-state. The question is about after the refactor.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    13. 13Car → EngineCode · C++ · Automotive

      A simulation models a car’s engine.

      C++
      1
      2
      3
      4
      5
      6
      
      class Car {
          std::unique_ptr<Engine> engine_;
      public:
          Car() : engine_(std::make_unique<Engine>()) {}
          void start() { engine_->ignite(); }
      };
      
      Show answer

      Answer Composition

      Deciding clue
      std::unique_ptr created in the constructor: exactly one owner, freed in Car’s destructor.
      Why
      unique_ptr encodes exclusive ownership in the type system. The car makes its own engine, nobody else can hold an owning pointer to it, and it’s destroyed when the car is. Even if the constructor took a unique_ptr<Engine> by move, it would still be composition, since ownership transfers and stays exclusive.
      Tempting wrong turn
      Dependency is tempting because start() merely calls the engine. The call is incidental; the stored unique_ptr is what defines the relationship.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    14. 14Order → AddressContext flip · E-commerce

      The order stores address_id pointing at a row in the customer’s address book. If the customer edits that address, the order now shows the new address.

      Show answer

      Answer Association

      Deciding clue
      The order references an independent, shared record by id.
      Why
      The address book entry exists independently and can be referenced by many orders, so this is a stored link, not ownership. (It’s also a real-world bug: past orders would silently change their delivery address.)
      Tempting wrong turn
      Aggregation is tempting because an order “has” an address. The address isn’t part of the order here; the order just points at it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    15. 15Report → ObjectMapperCode · Java · Reporting

      A report serializes itself to JSON.

      Java
      1
      2
      3
      4
      5
      6
      7
      
      class Report {
          private final List<Row> rows;
      
          String toJson() throws JsonProcessingException {
              return new ObjectMapper().writeValueAsString(rows);
          }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      The ObjectMapper is a temporary created inside one method and dropped when it returns.
      Why
      Creating an object isn’t owning it. Composition needs the created thing to be held as part of the whole’s state. Here nothing references the mapper after toJson() returns.
      Tempting wrong turn
      Composition is tempting because Report calls new ObjectMapper() itself. “Created inside” is a composition tell only when the result is stored.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    16. 16Server → ChannelScenario · Discord

      Channels are created inside a Discord server, can’t be moved to another server, and are deleted when the server is deleted.

      Show answer

      Answer Composition

      Deciding clue
      Created inside, never moves, dies with the server.
      Why
      Whole–part, the part can’t outlive the whole, and it can’t be shared. All three composition signals are present.
      Tempting wrong turn
      Aggregation would apply only if channels could survive the server or be shared with another server.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    17. 17Checkout → PricingStrategyCode · Python · Checkout

      A checkout receives its pricing strategy through the constructor. The same HourlyPricing instance is shared by every checkout.

      Python
      1
      2
      3
      4
      5
      6
      
      class Checkout:
          def __init__(self, pricing: PricingStrategy):
              self.pricing = pricing
      
          def total(self, cart: Cart) -> Money:
              return self.pricing.price(cart)
      
      Show answer

      Answer Association

      Also accepted Aggregation. Also accepted. Drawing ◇ signals “pluggable, shareable component”, and many diagrams draw strategies that way. The stricter reading is that a pricing policy isn’t a structural part of a checkout.

      Deciding clue
      The strategy is now stored in a field, so the link outlives every method call.
      Why
      Compared with passing the strategy into each call, the link is now stored, which moves it from dependency to association. Is a pricing policy a part of a checkout? Usually no: it’s a collaborator the checkout holds, so plain association is the stricter answer.
      Tempting wrong turn
      Composition is tempting because it’s set in the constructor. The instance is shared across checkouts, which rules out exclusive ownership.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    18. 18VendingMachine → CoinScenario · Vending machine

      insert(coin) checks the coin’s diameter and weight, adds its value to an integer balance, and drops the physical coin into the cash box. The VendingMachine object never stores Coin objects.

      Show answer

      Answer Dependency

      Deciding clue
      Only the coin’s value survives as an int; the Coin object isn’t kept.
      Why
      The machine reads the coin during insert() and then forgets it. Keeping a number derived from B isn’t keeping B.
      Tempting wrong turn
      Aggregation is tempting because the physical machine “contains” coins in its cash box. Model the software objects, not the hardware.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    19. 19TicTacToe → BoardCode · TypeScript · Tic-tac-toe

      A game class sets up its board.

      TypeScript
      1
      2
      3
      4
      5
      6
      7
      8
      
      class TicTacToe {
        private readonly board = new Board(3);
        private current: Player;
      
        constructor(private readonly players: [Player, Player]) {
          this.current = players[0];
        }
      }
      
      Show answer

      Answer Composition

      Deciding clue
      Created inside as a private readonly field, never exposed or accepted from outside.
      Why
      Each game builds its own board, no other game can share it, and it’s discarded with the game. Note the contrast with players, which are passed in and outlive the game.
      Tempting wrong turn
      Aggregation is tempting by analogy with the players. Look at who constructs each part: the game builds the board but receives the players.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    20. 20Car → EngineContext flip · Manufacturing

      At an engine-refurbishing shop, engines are tracked by serial number, pulled out of scrapped cars, rebuilt, and installed in other cars.

      Show answer

      Answer Aggregation

      Deciding clue
      The engine is still a part of a car, but it outlives cars and moves between them.
      Why
      Whole–part remains true while it’s installed, but the part has its own identity and lifetime. Swap the domain and the answer flips from composition to aggregation.
      Tempting wrong turn
      Composition is the answer at an automaker (#34). What changed is only the lifecycle rule, and that’s the whole point.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    21. 21Order → uuid moduleCode · Python · Orders

      An order generates its own id.

      Python
      1
      2
      3
      4
      5
      6
      
      import uuid
      
      class Order:
          def __init__(self, items: list[Item]):
              self.id = str(uuid.uuid4())
              self.items = items
      
      Show answer

      Answer Dependency

      Deciding clue
      Only the generated string is stored; nothing from the uuid module is kept.
      Why
      The order calls uuid4() once and keeps the result as a plain str. The order never holds the generator itself, so after __init__ returns there’s no link to it.
      Tempting wrong turn
      Association is tempting because something from the call is stored. Storing a value produced by B is not storing B.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    22. 22Pod → ContainerScenario · Kubernetes

      Containers are declared in the pod spec, share the pod’s network namespace, start with the pod, and are gone when the pod is deleted. You can’t move a running container into another pod.

      Show answer

      Answer Composition

      Deciding clue
      Declared inside the pod, share its namespaces, and die with it.
      Why
      A container’s runtime existence is entirely inside one pod. Whole–part, no independent lifetime, no sharing.
      Tempting wrong turn
      Aggregation is tempting because the same image can run in many pods. The image is shared; the container instance is not. Don’t confuse a template with an instance.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    23. 23Handler → StoreCode · Go · HTTP API

      An HTTP handler gets its storage through a constructor.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      type Handler struct {
          store Store
          log   *slog.Logger
      }
      
      func NewHandler(store Store, log *slog.Logger) *Handler {
          return &Handler{store: store, log: log}
      }
      
      func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
          v, err := h.store.Get(r.PathValue("key"))
          // ...
      }
      
      Show answer

      Answer Association

      Also accepted Dependency. Accepted as a team convention: many diagrams draw injected service collaborators as dashed dependencies to keep only domain relationships solid. Say which convention you’re using.

      Deciding clue
      The store is kept in a field after the constructor returns.
      Why
      By the strict test the handler still holds the store after any method returns, so it’s an association. It isn’t whole–part: the store is shared infrastructure the handler talks to.
      Tempting wrong turn
      Dependency feels right because “it’s just a service”. The strict test doesn’t care whether B is data or behavior.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    24. 24EntryGate → VehicleScenario · Parking lot

      EntryGate.scan(vehicle) reads the plate, asks the lot for a free spot, prints a ticket and returns it. The gate keeps no record of vehicles.

      Show answer

      Answer Dependency

      Deciding clue
      The vehicle is only a parameter of scan(); the gate stores nothing about it.
      Why
      The link lasts for a single call. Whatever needs to remember the vehicle (the ticket) is a different class with its own association.
      Tempting wrong turn
      Association is tempting because gates and vehicles interact constantly. Frequent interaction still isn’t stored state.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    25. 25Node → Node (via next)Code · Rust · Data structures

      A singly linked list in Rust.

      Rust
      1
      2
      3
      4
      
      struct Node {
          val: i32,
          next: Option<Box<Node>>,
      }
      
      Show answer

      Answer Composition

      Deciding clue
      Box<T> means unique ownership: dropping a node drops its entire tail.
      Why
      Rust makes the ownership explicit. Each node exclusively owns the next one, nothing else can own it, and it’s freed when its owner is. Compare with the Python version (#26), where the code can’t express ownership at all.
      Tempting wrong turn
      Association is the answer for the Python version (#26). Here the type system says “owns”, not “points to”.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    26. 26Directory → File (inode)Scenario · Linux filesystems

      On ext4, a file (an inode) can have several hard links in different directories. Removing one directory entry only frees the file when its link count reaches zero.

      Show answer

      Answer Aggregation

      Deciding clue
      The same file can be shared by several directories and outlives any one of them.
      Why
      A directory is a collection of entries (whole–part), but the underlying file is reference-counted and shared. Shared parts that survive the whole: aggregation.
      Tempting wrong turn
      Composition is the right answer for a simple LLD file system with no hard links. Real filesystems are more permissive than interview ones.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    27. 27VehicleFactory → CarCode · Python · Parking lot

      A factory builds vehicles from a type string.

      Python
      1
      2
      3
      4
      5
      6
      7
      
      class VehicleFactory:
          def create(self, kind: str) -> Vehicle:
              if kind == "car":
                  return Car()
              if kind == "truck":
                  return Truck()
              raise ValueError(kind)
      
      Show answer

      Answer Dependency

      Deciding clue
      The factory creates a Car and immediately hands it to the caller; it keeps nothing.
      Why
      This is the UML «create» flavor of dependency: the factory knows how to construct Car, but the relationship ends at return. The caller owns the result.
      Tempting wrong turn
      Composition is tempting because the factory calls Car(). Composition requires the creator to keep and own what it makes.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    28. 28Order → ShippingAddressContext flip · E-commerce

      At checkout, the order copies the customer’s shipping address into its own ShippingAddress value object. If the customer later edits their address book, existing orders don’t change.

      Show answer

      Answer Composition

      Deciding clue
      The order holds its own private snapshot that lives and dies with it.
      Why
      The snapshot belongs to exactly one order and is never shared. It disappears with the order. Snapshotting is also the correct business choice, because a shipped order must keep the address it shipped to.
      Tempting wrong turn
      Association is the answer when the order instead stores a reference to the address-book row (#33). A copy owned by the order is a part, not a link to the address book.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    29. 29Worker → context.ContextCode · Go · Go concurrency

      A worker keeps the context it was created with.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      
      type Worker struct {
          ctx  context.Context
          jobs chan Job
      }
      
      func NewWorker(ctx context.Context) *Worker {
          return &Worker{ctx: ctx, jobs: make(chan Job)}
      }
      
      Show answer

      Answer Association

      Deciding clue
      The context is stored in a struct field, so it outlives the constructor call.
      Why
      Stored, not whole–part: association. This is exactly why the context package docs say not to store contexts in structs and to pass ctx as the first parameter instead. They want it to stay a request-scoped dependency, not become a long-lived association.
      Tempting wrong turn
      Dependency is what idiomatic Go would give you, but this code stores it, so the code is an association.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    30. 30Tree → BlobScenario · Git internals

      Git stores file contents as blobs addressed by their hash. Two commits whose trees contain an identical README point at the same blob. When a tree becomes unreachable and git gc collects it, any blob still reachable from other trees is kept.

      Show answer

      Answer Aggregation

      Deciding clue
      Content-addressed blobs are shared between trees and outlive any one tree.
      Why
      A tree is made up of its entries (whole–part), but deduplication means blobs are shared and independently live. Shared, surviving parts: aggregation.
      Tempting wrong turn
      Composition is tempting because a tree is “just” its files. Dedup is precisely what breaks exclusive ownership.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    31. 31Account → InsufficientFundsExceptionCode · Java · Banking

      A withdrawal can fail.

      Java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      class Account {
          private long balance;
      
          void withdraw(long amount) {
              if (amount > balance)
                  throw new InsufficientFundsException(balance, amount);
              balance -= amount;
          }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      The exception is created and thrown, never stored.
      Why
      Thrown exceptions are a classic dependency: Account’s code is coupled to the exception type, but no Account instance ever holds one.
      Tempting wrong turn
      Inheritance is tempting if you’re thinking about the exception’s own superclass. The question is about Account → exception, and Account doesn’t extend it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    32. 32Car → EngineContext flip · Manufacturing

      In an automaker’s build system, an engine is assembled into exactly one car, isn’t tracked as a separate asset afterward, and is scrapped with the car.

      Show answer

      Answer Composition

      Deciding clue
      In this domain the engine has no life outside its car.
      Why
      Same two classes, but the domain rules decide: exclusive, not tracked separately, destroyed with the whole.
      Tempting wrong turn
      Aggregation is right for a different domain (see the refurbishing-shop version, #35). Never classify from the class names alone.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    33. 33StockTicker → SubscriberCode · Python · Observer pattern

      A ticker notifies subscribers without keeping them alive.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      import weakref
      
      class StockTicker:
          def __init__(self):
              self._subs: "weakref.WeakSet[Subscriber]" = weakref.WeakSet()
      
          def subscribe(self, s: Subscriber):
              self._subs.add(s)
      
          def publish(self, price: float):
              for s in self._subs:
                  s.on_price(price)
      
      Show answer

      Answer Association

      Deciding clue
      Subscribers are stored, but weakly: the ticker knows them without owning them.
      Why
      A weak reference is the code saying “I want to know you while you exist, but your lifetime isn’t my business.” Stored link, not whole–part: association. The subject–observer link in the Observer pattern is an association.
      Tempting wrong turn
      Aggregation is tempting because subscribers are kept in a collection. Being in a collection doesn’t make them parts of the ticker.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    34. 34Image → LayerScenario · Docker

      Image layers are content-addressed. Ten images built FROM the same base share that base’s layers on disk, and docker rmi removes only the layers no other image uses.

      Show answer

      Answer Aggregation

      Deciding clue
      Layers are shared across images and survive the removal of any one image.
      Why
      An image is composed of a stack of layers (whole–part), but those layers are shared and reference-counted. Aggregation.
      Tempting wrong turn
      Composition would hold only if each image had private layers that vanished with it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    35. 35Checkout → PricingStrategyCode · Python · Checkout

      A checkout is handed a pricing strategy on each call.

      Python
      1
      2
      3
      
      class Checkout:
          def total(self, cart: Cart, pricing: PricingStrategy) -> Money:
              return pricing.price(cart)
      
      Show answer

      Answer Dependency

      Deciding clue
      The strategy is a parameter of one method and isn’t stored.
      Why
      Each call can pass a different strategy, and the checkout remembers none of them.
      Tempting wrong turn
      Realization is tempting because a strategy pattern is involved. Realization is the arrow from a concrete strategy to the interface, not from the user of the strategy.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    36. 36Folder → FileScenario · LLD: file system

      In your interview design of an in-memory file system, every file has exactly one parent folder, and deleting a folder recursively deletes everything inside it.

      Show answer

      Answer Composition

      Deciding clue
      Exactly one parent, and recursive delete.
      Why
      These rules make the folder the sole owner: whole–part, no sharing, and the part can’t outlive the whole. This recursive composition is the Composite pattern (Folder contains Files and Folders).
      Tempting wrong turn
      Aggregation would apply if you added hard links or let files survive their folder.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    37. 37_Node → _Node (via next)Code · Python · Data structures

      A singly linked list in Python.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      class _Node:
          __slots__ = ("value", "next")
          def __init__(self, value, next=None):
              self.value = value
              self.next = next
      
      class LinkedList:
          def __init__(self):
              self._head = None
      
          def push(self, value):
              self._head = _Node(value, self._head)
      
      Show answer

      Answer Association

      Also accepted Composition. Defensible if you model each node as owning the rest of the chain (that’s literally what Rust’s Option<Box<Node>> encodes). In a garbage-collected language, the list is the real owner.

      Deciding clue
      The list owns the nodes; one node merely points at its neighbor.
      Why
      Ownership lives at the LinkedList level: the list creates nodes, keeps them private, and they die with it (LinkedList ◆ _Node). The next pointer is a stored navigation link between peers, a reflexive association.
      Tempting wrong turn
      Composition is tempting because the tail goes away with the node. What decides it here is that the design makes the list the owner and nothing states an exclusive-parent rule between nodes (compare #28, whose prompt states one).

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    38. 38Student ↔ CourseScenario · University

      A student takes many courses and a course has many students. For each pairing you must store the semester and the final grade.

      Show answer

      Answer Association class

      Deciding clue
      The link itself carries data (semester, grade).
      Why
      A grade belongs neither to the student alone nor to the course alone; it belongs to the pairing. When the link has attributes, promote it to a class: Enrollment.
      Tempting wrong turn
      Association is right only if you never need data about the pairing itself.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    39. 39BinaryOp → Expr (left/right fields)Code · Python · Compilers

      An AST node built by a parser. The parser builds every subtree fresh for exactly one parent and never reuses a node in two places; dropping a node drops its whole subtree.

      Python
      1
      2
      3
      4
      5
      6
      
      class BinaryOp(Expr):
          def __init__(self, op: str, left: Expr, right: Expr):
              self.op, self.left, self.right = op, left, right
      
      # inside the parser:
      #   node = BinaryOp("+", self.parse_term(), self.parse_term())
      
      Show answer

      Answer Composition

      Deciding clue
      Domain rule: each child belongs to exactly one parent and dies with it, even though it’s passed into the constructor.
      Why
      “Passed in vs. created inside” is a heuristic, not the definition. The lifecycle rules decide: exclusive parent, never shared, subtree dies with the node. That’s composition, and it’s the structure behind the Composite pattern.
      Tempting wrong turn
      Aggregation is tempting because the children arrive as constructor arguments. Don’t let a code tell override explicit lifecycle rules. (Also note BinaryOp → Expr via the base class is inheritance; this question is about the left/right fields.)

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    40. 40Branch → CommitScenario · Git internals

      A branch is a named pointer to one commit, its tip. Deleting the branch doesn’t delete the commit, and many branches can point at the same commit.

      Show answer

      Answer Association

      Deciding clue
      A branch points at a commit; it isn’t made of commits.
      Why
      Stored link: yes (the ref stores a commit hash). Whole–part: no. A branch is a movable label, not a container. So association.
      Tempting wrong turn
      Aggregation is tempting if you think of a branch as “containing its history”. Structurally it holds one pointer; history is reached by walking commit parents.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    41. 41User ↔ GroupSchema · SQL · Community app

      What does this table represent?

      SQL
      1
      2
      3
      4
      5
      6
      7
      
      CREATE TABLE memberships (
        user_id   BIGINT REFERENCES users(id),
        group_id  BIGINT REFERENCES groups(id),
        role      TEXT        NOT NULL,   -- 'owner' | 'admin' | 'member'
        joined_at TIMESTAMPTZ NOT NULL,
        PRIMARY KEY (user_id, group_id)
      );
      
      Show answer

      Answer Association class

      Deciding clue
      A join table with its own columns (role, joined_at).
      Why
      A many-to-many join table that carries data is the relational form of an association class: Membership.
      Tempting wrong turn
      Association is right for a bare join table with only the two foreign keys.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    42. 42A → BNotation · UML

      A class diagram shows a solid line from A to B with a filled diamond at A’s end.

      AB
      Show answer

      Answer Composition

      Deciding clue
      Filled diamond on the whole’s end.
      Why
      ◆ means composition: A is the whole, B is a part that lives and dies with it. The diamond always sits on the whole side.
      Tempting wrong turn
      Aggregation uses the same shape but hollow (◇).

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    43. 43Node → Node (via parent field)Code · Python · Trees

      A tree node keeps a pointer back to its parent.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      class Node:
          def __init__(self, parent: "Node | None" = None):
              self.parent = parent
              self.children: list[Node] = []
      
          def add_child(self) -> "Node":
              child = Node(parent=self)
              self.children.append(child)
              return child
      
      Show answer

      Answer Association

      Deciding clue
      It’s a back-reference: a child knows its parent but certainly doesn’t own it.
      Why
      The children list is composition (the node creates its children and they die with it). The parent field points the other way, and back-pointers never own. A stored link that isn’t whole–part from the child’s side is an association.
      Tempting wrong turn
      Composition is tempting because you see composition in the same class. One class can take part in several relationships; judge each field separately.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    44. 44Cart ↔ ProductScenario · E-commerce

      A cart can hold many products, a product can be in many carts, and for each pairing you must store the quantity and the price at the moment it was added.

      Show answer

      Answer Association class

      Deciding clue
      Quantity and captured price describe the pairing, not either side.
      Why
      This is CartItem. The cart then composes its CartItems, and each CartItem associates with one Product.
      Tempting wrong turn
      Aggregation is tempting because a cart “holds” products. A product can’t hold “quantity 3 in Tisan’s cart”; something in between has to.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    45. 45Library → BookSchema · SQL · Library

      What does this schema encode?

      SQL
      1
      2
      3
      4
      5
      6
      
      CREATE TABLE books (
        id          BIGINT PRIMARY KEY,
        title       TEXT NOT NULL,
        library_id  BIGINT NULL
                    REFERENCES libraries(id) ON DELETE SET NULL
      );
      
      Show answer

      Answer Aggregation

      Also accepted Association. Accepted: the schema alone only proves an optional, non-cascading link (a manager_id column looks identical). The whole–part reading comes from the domain: a library is its collection.

      Deciding clue
      A nullable FK with ON DELETE SET NULL: the book survives its library.
      Why
      The book belongs to a library (whole–part in this domain) but keeps living, unshelved, when the library is deleted. SET NULL is consistent with aggregation; CASCADE would signal composition.
      Tempting wrong turn
      Composition would use NOT NULL with CASCADE.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    46. 46A → BNotation · UML

      A class diagram shows a dashed line from A to B with an open arrowhead at B.

      AB
      Show answer

      Answer Dependency

      Deciding clue
      Dashed line, open arrowhead.
      Why
      The dashed open arrow means A uses B somewhere in its code (a parameter, local variable, return type or static call) but doesn’t keep it.
      Tempting wrong turn
      Realization is also dashed, but ends in a hollow triangle rather than an open arrowhead.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    47. 47Show ↔ SeatScenario · Movie ticketing

      A seat (say, A9) physically belongs to a screen. Each show on that screen needs its own status for every seat (available, locked, booked) and its own price.

      Show answer

      Answer Association class

      Deciding clue
      Status and price belong to (show, seat), not to the seat or the show alone.
      Why
      This is ShowSeat: per-show inventory. Screen ◆ Seat is composition (the physical seat), while ShowSeat is the association class between Show and Seat, and it’s what gets locked during booking.
      Tempting wrong turn
      Composition is right for Screen → Seat, which is a different pair.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    48. 48Book ↔ AuthorSchema · SQL · Publishing

      What does this table represent?

      SQL
      1
      2
      3
      4
      5
      
      CREATE TABLE book_authors (
        book_id   BIGINT REFERENCES books(id),
        author_id BIGINT REFERENCES authors(id),
        PRIMARY KEY (book_id, author_id)
      );
      
      Show answer

      Answer Association

      Deciding clue
      A bare join table: two foreign keys and nothing else.
      Why
      Many-to-many with no data on the link is a plain association. Neither side is part of the other, and deleting a book doesn’t delete its authors.
      Tempting wrong turn
      Association class needs attributes on the link (author order or a royalty split, say). There are none here.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    49. 49A → BNotation · UML

      A class diagram shows a solid line from A to B with a hollow diamond at A’s end.

      AB
      Show answer

      Answer Aggregation

      Deciding clue
      Hollow diamond on the whole’s end.
      Why
      ◇ means aggregation: A is a whole made of B parts, but B can outlive A or be shared.
      Tempting wrong turn
      Composition is the filled version. A quick way to remember: filled means fully owned.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    50. 50Admin → UserSchema · SQL · SaaS accounts

      What object-model relationship does this table implement?

      SQL
      1
      2
      3
      4
      5
      6
      
      CREATE TABLE users (
        id          BIGINT PRIMARY KEY,
        type        TEXT NOT NULL CHECK (type IN ('member', 'admin')),
        email       TEXT NOT NULL,
        admin_level INT  NULL   -- only set when type = 'admin'
      );
      
      Show answer

      Answer Inheritance

      Deciding clue
      A type discriminator plus subtype-only columns in one table.
      Why
      This is single-table inheritance: Admin is a kind of User with extra fields. ORMs map exactly this shape to a subclass.
      Tempting wrong turn
      Composition or association would need a second table or a foreign key; there’s no relationship between two rows here, only between two types.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    51. 51A → BNotation · UML

      A class diagram shows a dashed line from A to B ending in a hollow triangle at B.

      AB
      Show answer

      Answer Realization

      Deciding clue
      Dashed line, hollow triangle: A implements B’s contract.
      Why
      Realization: B is an interface (or protocol or trait) and A promises its behavior without inheriting an implementation.
      Tempting wrong turn
      Inheritance uses a solid line. Dashed means a contract, solid means a real superclass.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    52. 52Order → CustomerSchema · SQL · E-commerce

      What does this schema encode?

      SQL
      1
      2
      3
      4
      5
      6
      
      CREATE TABLE orders (
        id           BIGINT PRIMARY KEY,
        customer_id  BIGINT NOT NULL
                     REFERENCES customers(id) ON DELETE RESTRICT,
        placed_at    TIMESTAMPTZ NOT NULL
      );
      
      Show answer

      Answer Association

      Deciding clue
      Every order must reference a customer, but neither is part of the other.
      Why
      NOT NULL says the link is mandatory; RESTRICT says you can’t delete a customer who has orders. A customer isn’t a part of an order. Orders are also kept as records rather than dying with the customer. Mandatory doesn’t mean owned.
      Tempting wrong turn
      Composition is tempting because of NOT NULL. Composition is about cascading lifetime, and RESTRICT explicitly refuses to cascade.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    53. 53A → BNotation · UML

      A class diagram shows a solid line from A to B ending in a hollow triangle at B.

      AB
      Show answer

      Answer Inheritance

      Deciding clue
      Solid line, hollow triangle pointing at the parent.
      Why
      This is generalization: A is a kind of B, and the triangle points at the superclass.
      Tempting wrong turn
      Realization is the dashed version of the same arrow.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    54. 54Post → CommentSchema · SQL · Blog

      What does this schema encode between posts and comments?

      SQL
      1
      2
      3
      4
      5
      6
      7
      
      CREATE TABLE comments (
        id        BIGINT PRIMARY KEY,
        post_id   BIGINT NOT NULL
                  REFERENCES posts(id) ON DELETE CASCADE,
        author_id BIGINT NOT NULL REFERENCES users(id),
        body      TEXT   NOT NULL
      );
      
      Show answer

      Answer Composition

      Deciding clue
      post_id is NOT NULL with ON DELETE CASCADE.
      Why
      Every comment belongs to exactly one post and dies with it: whole–part with no independent lifetime.
      Tempting wrong turn
      Aggregation would use a nullable post_id with SET NULL.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    55. 55A ↔ B, with C attachedNotation · UML

      A solid line connects A and B. A dashed line runs from a third class box, C, to the middle of that line.

      ABC
      Show answer

      Answer Association class

      Deciding clue
      A class hanging off the middle of an association line.
      Why
      This is UML’s association-class notation: C holds the data that belongs to the A–B link (for example, Enrollment between Student and Course).
      Tempting wrong turn
      Dependency is tempting because of the dashed line. Here the dashed line attaches to a relationship, not to a class.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    56. 56Comment → User (author)Schema · SQL · Blog

      What is the relationship between a comment and its author in this schema?

      SQL
      1
      2
      3
      4
      5
      6
      7
      
      CREATE TABLE comments (
        id        BIGINT PRIMARY KEY,
        post_id   BIGINT NOT NULL
                  REFERENCES posts(id) ON DELETE CASCADE,
        author_id BIGINT NOT NULL REFERENCES users(id),
        body      TEXT   NOT NULL
      );
      
      Show answer

      Answer Association

      Deciding clue
      author_id is a mandatory reference with no cascade, and a user is not a part of a comment.
      Why
      The comment knows its author persistently, but users exist independently and write many comments. One table can encode composition in one column and association in another.
      Tempting wrong turn
      Composition is tempting because the post_id column in this same table does encode composition (#49). Judge each column separately.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    57. 57Calculator → NumberSpot the error · Calculator

      A diagram shows Calculator ◆—— Number, drawn because add(a: Number, b: Number) takes Numbers. Calculator has no Number fields. What should it be?

      Show answer

      Answer Dependency

      Deciding clue
      Number appears only in a method signature.
      Why
      Parameters are dependencies. Nothing is stored, so there’s no association at all, let alone composition.
      Tempting wrong turn
      Composition is the error being tested: a method signature never implies ownership.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    58. 58Stack → ArrayListCode · Java · Data structures

      What relationship does this code actually create?

      Java
      1
      2
      3
      4
      
      class Stack<T> extends ArrayList<T> {
          void push(T x) { add(x); }
          T pop()        { return remove(size() - 1); }
      }
      
      Show answer

      Answer Inheritance

      Deciding clue
      extends ArrayList.
      Why
      The code says “a Stack is an ArrayList”. It’s a classic design smell: Stack now exposes add(i, x), get(i) and remove(i), which break stack semantics. The better design is composition, with a private list field (Java’s own Stack extends Vector is the historical example of this mistake).
      Tempting wrong turn
      Composition is what it should be, not what it is. Classify the code you’re given, then critique it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    59. 59A ↔ BNotation · UML

      A plain solid line connects A and B, labelled 1 near A and 0..* near B. There are no diamonds, triangles or arrowheads.

      AB10..*
      Show answer

      Answer Association

      Deciding clue
      A plain solid line with multiplicities.
      Why
      A plain solid line is an association. Multiplicities only make sense for stored links, which is another way to tell it apart from a dependency.
      Tempting wrong turn
      Aggregation needs a diamond. Multiplicity alone doesn’t imply whole–part.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    60. 60Team → EmployeeSpot the error · HR

      A diagram shows Team ◆—— Employee (composition). In the system, when a team is dissolved its people are reassigned to other teams. What should it be?

      Show answer

      Answer Aggregation

      Deciding clue
      The parts survive the whole being dissolved.
      Why
      Still whole–part (a team is its members), but employees outlive the team, so the diamond should be hollow.
      Tempting wrong turn
      Association is defensible-sounding, but a team really is defined by its members, which is the whole–part signal.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    61. 61Circle → ShapeCode · Python · Geometry

      A shape hierarchy with an abstract base class.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      from abc import ABC, abstractmethod
      import math
      
      class Shape(ABC):
          @abstractmethod
          def area(self) -> float: ...
      
          def describe(self) -> str:
              return f"{type(self).__name__}, area {self.area():.2f}"
      
      class Circle(Shape):
          def __init__(self, r: float):
              self.r = r
      
          def area(self) -> float:
              return math.pi * self.r ** 2
      
      Show answer

      Answer Inheritance

      Deciding clue
      Circle subclasses an abstract class and inherits the concrete describe() method.
      Why
      Subclassing an abstract class is generalization (inheritance), not realization, because Circle inherits real behavior, describe(). Realization is for pure contracts with nothing to inherit.
      Tempting wrong turn
      Realization is tempting because of ABC and @abstractmethod. The presence of an inherited implementation settles it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    62. 62Doctor → PatientSpot the error · Hospital

      A teammate drew Doctor ◇—— Patient (aggregation), reasoning that “a doctor has patients”. Each doctor stores a list of their current patients; patients exist independently and can see several doctors. What should it be?

      Show answer

      Answer Association

      Deciding clue
      “Has” in English isn’t whole–part. A doctor isn’t made of patients.
      Why
      The doctor stores the link, so it’s at least an association. But a patient isn’t structurally a part of a doctor, so it’s not aggregation. Don’t let everyday words choose your diamond.
      Tempting wrong turn
      Aggregation is the teammate’s mistake: reading possession-language as whole–part.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    63. 63Circle → DrawableCode · Python · Graphics

      Note that Circle has no base class.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      from typing import Protocol
      
      class Drawable(Protocol):
          def draw(self, canvas: "Canvas") -> None: ...
      
      class Circle:
          def __init__(self, x: float, y: float, r: float):
              self.x, self.y, self.r = x, y, r
      
          def draw(self, canvas: "Canvas") -> None:
              canvas.ellipse(self.x, self.y, self.r, self.r)
      
      def render(items: list[Drawable], canvas: "Canvas"): ...
      
      Show answer

      Answer Realization

      Deciding clue
      Circle satisfies the Drawable protocol structurally, with no inheritance.
      Why
      A Protocol is a pure contract, and any class with a matching draw() conforms. Conforming to a contract without inheriting anything is realization, even when it’s implicit.
      Tempting wrong turn
      “No relationship” or dependency is tempting because Circle never mentions Drawable. Implicit conformance is still realization (Go interfaces work the same way).

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    64. 64Invoice → LineItemSpot the error · Billing

      A diagram shows Invoice ◇—— LineItem (aggregation). Line items are created by the invoice, numbered within it, and deleted with it. What should it be?

      Show answer

      Answer Composition

      Deciding clue
      Created by the whole, identified within it, and deleted with it.
      Why
      Every lifecycle signal says exclusive ownership. The diamond should be filled.
      Tempting wrong turn
      Aggregation is the mistake: a line item can’t exist without its invoice.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    65. 65StripeGateway → CardCode · Java · Payments

      What is the relationship between the gateway and the card it charges?

      Java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      interface PaymentGateway {
          Receipt charge(Money amount, Card card);
      }
      
      class StripeGateway implements PaymentGateway {
          public Receipt charge(Money amount, Card card) {
              String token = card.tokenize();
              // call Stripe with token...
              return new Receipt(/* ... */);
          }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      Card appears only as a parameter of charge().
      Why
      The gateway reads the card for one call and keeps nothing. (StripeGateway → Receipt is also a dependency, because it’s created and returned.)
      Tempting wrong turn
      Association is tempting because payment cards are important domain objects. Importance doesn’t make a link persistent.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    66. 66ConsoleLogger → LoggerSpot the error · Logging

      A diagram draws ConsoleLogger ——▷ Logger with a solid line and hollow triangle. Logger is a Java interface with no fields and no method bodies. What should it be?

      Show answer

      Answer Realization

      Deciding clue
      The target is an interface, so the arrow should be dashed.
      Why
      Implementing an interface is realization (dashed line, hollow triangle). A solid line claims ConsoleLogger inherits an implementation, and there is none to inherit.
      Tempting wrong turn
      Inheritance is what the diagram currently shows.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    67. 67Parser → input &strCode · Rust · Parsing

      A zero-copy parser.

      Rust
      1
      2
      3
      4
      5
      6
      7
      8
      
      struct Parser<'a> {
          input: &'a str,
          pos: usize,
      }
      
      impl<'a> Parser<'a> {
          fn new(input: &'a str) -> Self { Parser { input, pos: 0 } }
      }
      
      Show answer

      Answer Association

      Deciding clue
      The parser stores a borrowed reference for its whole life but doesn’t own the text.
      Why
      Stored, so not a dependency. Borrowed, so not owned. The lifetime ‘a even forces the parser to die before the input does, which is the opposite of composition, where the part dies with the whole.
      Tempting wrong turn
      Composition is tempting because it’s a struct field. A field holding a borrow is a link, not ownership; Rust spells out the difference.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    68. 68ParkingLot → VehicleSpot the error · Parking lot

      A diagram shows ParkingLot ◆—— Vehicle, “since the lot contains the vehicles parked in it”. In the code, ParkingLot only receives vehicles as an argument to park() and unpark(); occupancy is recorded in ParkingSpot.current_vehicle. What should ParkingLot → Vehicle be?

      Show answer

      Answer Dependency

      Deciding clue
      The lot only sees vehicles as method parameters; ParkingSpot is what stores them.
      Why
      Vehicles exist before and after parking, so composition is wrong on lifecycle alone. In this design the lot doesn’t even store them, which makes it a dependency. The association lives on ParkingSpot → Vehicle.
      Tempting wrong turn
      Association is right for ParkingSpot → Vehicle, not for the lot.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    69. 69FileStore → StoreCode · Go · Storage

      A key-value store abstraction in Go.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      type Store interface {
          Get(key string) ([]byte, error)
          Put(key string, val []byte) error
      }
      
      type FileStore struct{ dir string }
      
      func (f *FileStore) Get(key string) ([]byte, error)    { /* ... */ }
      func (f *FileStore) Put(key string, val []byte) error  { /* ... */ }
      
      Show answer

      Answer Realization

      Deciding clue
      *FileStore has Store’s full method set, so it implements Store implicitly.
      Why
      Go has no implements keyword, but satisfying an interface’s method set is exactly realization. Diagram it as FileStore - - -▷ Store. (Strictly it is *FileStore that satisfies Store, because the methods have pointer receivers; a FileStore value isn’t assignable to Store.)
      Tempting wrong turn
      Inheritance is impossible here: Go has no class inheritance.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    70. 70Elevator → FloorScenario · Elevator system

      Each Elevator keeps a reference to the Floor object it’s currently at, updated as it moves. Floors belong to the Building.

      Show answer

      Answer Association

      Deciding clue
      A stored, changing reference to an object owned by someone else.
      Why
      The elevator remembers its current floor (stored), but a floor isn’t a part of an elevator (Building ◆ Floor is the ownership). A stored link that isn’t whole–part is an association.
      Tempting wrong turn
      Aggregation is tempting because the reference persists. Persistence gets you to association; whole–part is a separate question.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    71. 71Renderer → ConfigCode · Rust · Rendering

      A renderer reads its configuration per call.

      Rust
      1
      2
      3
      4
      5
      6
      7
      
      struct Renderer { width: u32, height: u32 }
      
      impl Renderer {
          fn render(&self, scene: &Scene, cfg: &Config) -> Image {
              // ...
          }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      cfg is borrowed as a parameter, and the borrow ends when render returns.
      Why
      The struct has no Config field. Rust makes it extra clear: the borrow can’t outlive the call.
      Tempting wrong turn
      Association is tempting because every render needs a config. It’s needed every time but stored never.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    72. 72Building → FloorScenario · Elevator system

      Floors are created when the Building is modeled. “Floor 3” has no meaning on its own, and removing the building from the system removes its floors.

      Show answer

      Answer Composition

      Deciding clue
      Floors are created with the building and have no meaning or life outside it.
      Why
      Whole–part, created by the whole, and dies with it. This is the composition the elevator’s association points into.
      Tempting wrong turn
      Aggregation would need floors to survive or be shared, and neither is possible.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    73. 73StripeGateway → PaymentGatewayCode · Java · Payments

      A payment gateway abstraction.

      Java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      interface PaymentGateway {
          Receipt charge(Money amount, Card card);
      }
      
      class StripeGateway implements PaymentGateway {
          public Receipt charge(Money amount, Card card) {
              String token = card.tokenize();
              // call Stripe with token...
              return new Receipt(/* ... */);
          }
      }
      
      Show answer

      Answer Realization

      Deciding clue
      implements PaymentGateway.
      Why
      StripeGateway fulfils an interface’s contract: realization, drawn with a dashed line and hollow triangle.
      Tempting wrong turn
      Inheritance would be extends with a concrete superclass.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    74. 74Dispatcher → HallCallScenario · Elevator system

      The Dispatcher receives a HallCall(floor=7, direction=UP), picks the best elevator, appends 7 to that elevator’s stop queue (a list of ints), and discards the HallCall object.

      Show answer

      Answer Dependency

      Deciding clue
      The call object is read and discarded; only the integer 7 survives, inside a different object.
      Why
      Within dispatch() the dispatcher uses the HallCall, then holds nothing. Extracting a value and storing it elsewhere doesn’t make the original object stored.
      Tempting wrong turn
      Aggregation is tempting because pending calls feel like a queue the dispatcher owns. The dispatcher doesn’t keep the call objects in this design.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    75. 75Fleet → TruckCode · C++ · Logistics

      Trucks are transferred between regional fleets.

      C++
      1
      2
      3
      4
      5
      6
      7
      
      class Fleet {
          std::vector<std::shared_ptr<Truck>> trucks_;
      public:
          void add(std::shared_ptr<Truck> t) {
              trucks_.push_back(std::move(t));
          }
      };
      
      Show answer

      Answer Aggregation

      Deciding clue
      shared_ptr and a passed-in part: shared ownership, so the truck can outlive this fleet.
      Why
      A fleet is made up of trucks (whole–part), and shared_ptr says no single fleet decides a truck’s lifetime. That’s aggregation.
      Tempting wrong turn
      Composition would use unique_ptr or value members created inside.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    76. 76Piece → SquareScenario · Chess

      Each Piece has a position field pointing at the Square it currently occupies, which changes on every move. Squares are owned by the Board.

      Show answer

      Answer Association

      Deciding clue
      A stored link to an object owned by the board.
      Why
      The piece remembers where it is, but a square isn’t part of a piece. Board ◆ Square is the ownership; Piece → Square is a navigational association that changes over time.
      Tempting wrong turn
      Composition is tempting if you mix it up with Board → Square.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    77. 77Money → DisplayCode · Rust · Money

      Formatting money for output.

      Rust
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      use std::fmt;
      
      struct Money { cents: i64, currency: Currency }
      
      impl fmt::Display for Money {
          fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
              write!(f, "{} {:.2}", self.currency, self.cents as f64 / 100.0)
          }
      }
      
      Show answer

      Answer Realization

      Deciding clue
      impl Display for Money: implementing a trait.
      Why
      A trait is a contract, and implementing it is realization. Rust has no struct inheritance at all.
      Tempting wrong turn
      Dependency is tempting because Display comes from the standard library. Implementing a contract is a stronger statement than merely using a type.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, implements an interface
      2. Realization
    78. 78Email → AttachmentScenario · Email

      An attachment is encoded inside one email’s MIME body. Forwarding the email creates a new copy of the attachment, and deleting the email deletes it.

      Show answer

      Answer Composition

      Deciding clue
      Each email has its own copy, and it dies with the email.
      Why
      Forwarding copies rather than shares, so ownership stays exclusive. Whole–part with no independent lifetime: composition.
      Tempting wrong turn
      Aggregation would apply if the attachment were stored once in a shared blob store and referenced by many emails (as some mail servers dedupe). The rules given here say copy, not share.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    79. 79Employee → DepartmentCode · C++ · HR

      Departments are created before employees and outlive them.

      C++
      1
      2
      3
      4
      5
      6
      
      class Employee {
          Department* dept_;   // non-owning
      public:
          explicit Employee(Department* d) : dept_(d) {}
          const Department& department() const { return *dept_; }
      };
      
      Show answer

      Answer Association

      Deciding clue
      A raw, explicitly non-owning pointer to something that isn’t a part of the employee.
      Why
      Stored link, so association. The employee isn’t the whole and the department isn’t its part (if anything, it’s the other way round), so there’s no diamond on this end.
      Tempting wrong turn
      Aggregation is tempting if you read the pair backwards (Department ◇ Employee). Check the direction you’re asked about.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    80. 80Album → PhotoScenario · Photos app

      Every photo lives in your library. Albums group photos; one photo can be in several albums, and deleting an album leaves its photos in the library.

      Show answer

      Answer Aggregation

      Also accepted Association. Accepted: albums sit on the blurry aggregation/association line. Aggregation is preferred because an album is defined by the photos in it.

      Deciding clue
      Albums are collections of photos that live elsewhere and can be shared.
      Why
      An album is made of photos (whole–part), but those photos outlive it and appear in other albums. Aggregation.
      Tempting wrong turn
      Composition would hold if deleting an album deleted its photos, which some older apps actually did.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    81. 81SignupValidator → ValidationResultCode · TypeScript · Sign-up flow

      A validator returns a fresh result object on every call.

      TypeScript
      1
      2
      3
      4
      5
      6
      7
      8
      
      class SignupValidator {
        validate(form: SignupForm): ValidationResult {
          const errors: string[] = [];
          if (!form.email.includes("@")) errors.push("email");
          if (form.password.length < 12) errors.push("password");
          return new ValidationResult(errors);
        }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      The result is created and returned; the validator keeps nothing.
      Why
      A return type that isn’t stored is a dependency (and so is the SignupForm parameter). The validator is stateless, so it can’t hold an association to anything.
      Tempting wrong turn
      Composition is tempting because the validator creates the result. Created and handed away is «create» dependency, not ownership.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    82. 82Event → ReminderScenario · Calendar

      Reminders are configured per event (“10 minutes before”). Deleting the event deletes its reminders, and a reminder can’t be attached to a different event.

      Show answer

      Answer Composition

      Deciding clue
      Configured per event, can’t move, and dies with the event.
      Why
      A reminder only makes sense relative to its event. Whole–part, exclusive, same lifetime.
      Tempting wrong turn
      Association is tempting because a reminder could be seen as a separate notification object. Its existence is fully tied to one event.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    83. 83Dashboard → WidgetCode · TypeScript · Monitoring

      Widgets are reusable across dashboards.

      TypeScript
      1
      2
      3
      4
      5
      6
      7
      8
      
      class Dashboard {
        constructor(public title: string, private widgets: Widget[]) {}
        render() { return this.widgets.map(w => w.render()); }
      }
      
      const cpu  = new CpuWidget();
      const ops  = new Dashboard("Ops",  [cpu, new LatencyWidget()]);
      const exec = new Dashboard("Exec", [cpu]);   // same instance
      
      Show answer

      Answer Aggregation

      Deciding clue
      The same widget instance sits in two dashboards.
      Why
      A dashboard is composed of widgets (whole–part), but widgets are created outside and shared. Sharing rules out composition, leaving aggregation.
      Tempting wrong turn
      Composition is tempting because a dashboard is its widgets visually. The shared cpu instance disproves exclusive ownership.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    84. 84User → User (follows)Scenario · Social network

      Users follow other users. A follow stores nothing except who follows whom.

      Show answer

      Answer Association

      Deciding clue
      A stored, directed link between peers of the same class, carrying no data.
      Why
      Reflexive, directed association. If you later add followed_at or notification settings, it becomes an association class (Follow).
      Tempting wrong turn
      Association class needs data on the link, and this follow has none.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    85. 85HealthHandler → http.RequestCode · Go · HTTP

      A health-check endpoint.

      Go
      1
      2
      3
      4
      5
      
      type HealthHandler struct{ started time.Time }
      
      func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
          fmt.Fprintf(w, "up %s", time.Since(h.started))
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      The request exists only as a parameter of ServeHTTP.
      Why
      Each request is handled and forgotten; the handler has no request field. Request-scoped objects are almost always dependencies of the handler.
      Tempting wrong turn
      Association is tempting because a handler’s entire job is requests. Its job is to process them, not to remember them.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    86. 86Fleet → AircraftScenario · Aviation

      An airline’s Fleet object lists the aircraft it operates. Aircraft are leased: when a lease ends, the same aircraft, with the same registration, joins another airline’s fleet.

      Show answer

      Answer Aggregation

      Deciding clue
      An aircraft is part of a fleet for a while, then moves to another.
      Why
      A fleet is made up of aircraft (whole–part), but the aircraft has its own identity and outlives the relationship. Aggregation.
      Tempting wrong turn
      Association is tempting because the aircraft is an independent asset (compare Employee → Laptop). The difference is the whole: a fleet is made of aircraft, while an employee is not made of laptops.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? Yes
      6. Aggregation
    87. 87Person → AddressCode · Python · Profiles

      Address is an immutable value. Changing your address means replacing it with a new Address, never sharing one instance between people.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      from dataclasses import dataclass, field
      
      @dataclass(frozen=True)
      class Address:
          line1: str
          city: str
          pin: str
      
      @dataclass
      class Person:
          name: str
          home: Address = field(default_factory=lambda: Address("", "", ""))
      
      Show answer

      Answer Composition

      Deciding clue
      An immutable value object owned by exactly one person.
      Why
      Value objects are the cleanest composition: no identity of their own, never shared by policy, and gone when their owner is gone.
      Tempting wrong turn
      Association is tempting because Address looks like a standalone class. A value object with no identity can’t be the far end of a link between peers.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    88. 88User ↔ CompanyScenario · Professional network

      A user can work at several companies over a career, and a company has many employees. For each pairing you store a title, a start date and an optional end date.

      Show answer

      Answer Association class

      Deciding clue
      Title and dates describe the employment, not the user or the company.
      Why
      The data belongs to the pairing, so promote it: Position (or Employment). If the same user can hold two positions at one company over time, each link needs its own identity: in strict UML, mark the association ends {nonunique} or model Position as a full class with two associations.
      Tempting wrong turn
      Association loses the title and dates.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    89. 89PaymentService → LoggerCode · Python · Payments

      A module-level logger, used inside a class.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      
      import logging
      
      log = logging.getLogger(__name__)
      
      class PaymentService:
          def refund(self, payment_id: str) -> None:
              log.info("refunding %s", payment_id)
              ...
      
      Show answer

      Answer Dependency

      Also accepted Association. Some designers treat a module-level singleton the class always uses as an association. By the strict test it’s a dependency: no PaymentService instance holds the logger.

      Deciding clue
      The logger is a module global; no PaymentService instance stores it.
      Why
      The class’s code reaches for a global during refund(). No instance keeps a reference, so the strict test says dependency, the same as a static call.
      Tempting wrong turn
      Association is tempting because the logger is used everywhere and lives forever. Long-lived B doesn’t create a link unless A holds it.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    90. 90Employee → LaptopScenario · IT asset tracking

      IT assigns each laptop to one employee at a time. When someone leaves, their laptop is wiped and reassigned. Laptops are tracked by serial number.

      Show answer

      Answer Association

      Deciding clue
      An assignment link to an independently tracked asset that isn’t a part of the employee.
      Why
      The link is stored, so it’s not a dependency. An employee isn’t made up of laptops, so it’s not whole–part. If you later need assigned_on and returned_on dates, it becomes an association class (Assignment).
      Tempting wrong turn
      Aggregation is tempting because the employee “has” a laptop. Possession isn’t composition of parts.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    91. 91LinkedList → NodeCode · Java · Data structures

      A linked list with a private nested node class.

      Java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      public class LinkedList<T> {
          private Node<T> head;
          private int size;
      
          private static class Node<T> {
              T value;
              Node<T> next;
              Node(T v, Node<T> n) { value = v; next = n; }
          }
      
          public void addFirst(T v) { head = new Node<>(v, head); size++; }
      }
      
      Show answer

      Answer Composition

      Deciding clue
      Node is private, created only by the list, and invisible outside it.
      Why
      The list is the sole creator and owner of its nodes. They’re an implementation detail that dies with the list. (Node → Node via next is a separate association between peers.)
      Tempting wrong turn
      Association is right for the next pointer, not for the list-to-node relationship.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    92. 92Playlist ↔ SongScenario · Music streaming

      The product team now wants every playlist row to show who added the song and when, and to keep a manual position. The same song can appear twice in one playlist.

      Show answer

      Answer Association class

      Deciding clue
      Added-by, added-at and position belong to each entry, and duplicates need separate identities.
      Why
      A plain playlist-to-song link can’t hold “added by Asha on Monday at position 4”, and it can’t tell two copies of the same song apart. Each entry becomes an object: PlaylistEntry, an association class (the playlist composes its entries). Strictly, allowing the same song twice needs {nonunique} ends or PlaylistEntry as a full class, because a default association class allows one link per pair.
      Tempting wrong turn
      Aggregation was the answer before the link gained data. Requirements changed, so the model must change.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    93. 93Leaderboard → ComparatorCode · Java · Gaming

      A leaderboard sorts on demand.

      Java
      1
      2
      3
      4
      5
      6
      7
      8
      
      class Leaderboard {
          private final List<Player> players = new ArrayList<>();
      
          List<Player> top(int n) {
              players.sort(Comparator.comparingInt(Player::score).reversed());
              return players.subList(0, Math.min(n, players.size()));
          }
      }
      
      Show answer

      Answer Dependency

      Deciding clue
      The comparator is built and used inside top() and never stored.
      Why
      A local, short-lived object used during one method: dependency. Compare with Leaderboard → Player, which is stored in a field.
      Tempting wrong turn
      Composition is tempting because the leaderboard creates the comparator. It isn’t held as state.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? No
      3. Dependency
    94. 94Recipe ↔ IngredientScenario · Recipes

      Ingredients (flour, butter) are a shared catalog. Each recipe uses many ingredients, and for each one you store a quantity and a unit.

      Show answer

      Answer Association class

      Deciding clue
      “250 g” belongs to (this recipe, flour), not to flour itself.
      Why
      Quantity and unit describe the pairing, so promote the link to RecipeIngredient.
      Tempting wrong turn
      Composition is tempting because recipes “contain” ingredients. But flour is a shared catalog item, not owned by any recipe.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    95. 95Logger → AppenderCode · Java · Logging framework

      Appenders are configured once and attached to several loggers.

      Java
      1
      2
      3
      4
      5
      6
      7
      
      Appender file = new FileAppender("app.log");
      
      Logger http = Logger.get("http");
      Logger db   = Logger.get("db");
      
      http.addAppender(file);
      db.addAppender(file);   // the same appender, shared
      
      Show answer

      Answer Association

      Also accepted Aggregation. Accepted if you read a logger’s configured appender set as its parts. The stricter reading treats appenders like subscribers: outputs the logger sends to, not pieces of it.

      Deciding clue
      The logger stores appenders across calls, and one appender instance is shared by two loggers.
      Why
      Stored, so not a dependency. Shared, so not composition. Is an appender a structural part of a logger? It’s closer to a subscriber: an output the logger sends events to. That makes association the strict answer, consistent with Checkout → PricingStrategy (#21) and StockTicker → Subscriber (#25).
      Tempting wrong turn
      Dependency is tempting because the logger “just writes to” an appender. It keeps the appender across calls, so the link is stored.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? No
      5. Association
    96. 96Employee ↔ ProjectScenario · Resource planning

      Employees are staffed on several projects at once. Each staffing has a role (lead, reviewer) and a percentage allocation.

      Show answer

      Answer Association class

      Deciding clue
      Role and allocation exist only for a specific employee on a specific project.
      Why
      This is Assignment, an association class carrying data that neither endpoint can hold alone.
      Tempting wrong turn
      Aggregation is tempting because a project “has” staff. The data on each staffing is what forces a class.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? Yes
      4. Association class
    97. 97BillingService → LoggerCode · Go · Billing

      Go struct embedding.

      Go
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      type Logger struct{ prefix string }
      
      func (l Logger) Info(msg string) { fmt.Println(l.prefix, msg) }
      
      type BillingService struct {
          Logger                      // embedded by value
          rates map[string]float64
      }
      
      // svc.Info("charged") works through method promotion
      
      Show answer

      Answer Composition

      Deciding clue
      Embedding by value places the Logger inside BillingService’s memory; promotion is only syntax.
      Why
      An embedded value field is still a field: created with the struct, destroyed with it, never shared. Method promotion lets you call svc.Info(), but a BillingService still can’t be passed where a Logger is expected, so there’s no subtyping.
      Tempting wrong turn
      Inheritance is the classic misreading of embedding. Go has no inheritance; embedding is composition plus delegation sugar.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? No
      2. Is the link stored, so it still exists after the method returns? Yes
      3. Does the link itself carry data (quantity, status, dates, role)? No
      4. Is it whole–part: is A made of B? Yes
      5. Can the part outlive the whole, or be shared by another whole? No
      6. Composition
    98. 98InsufficientFunds → PaymentErrorCode · Python · Payments

      An exception hierarchy.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      class PaymentError(Exception):
          def __init__(self, payment_id: str, msg: str):
              super().__init__(msg)
              self.payment_id = payment_id
      
      class InsufficientFunds(PaymentError):
          pass
      
      try:
          charge(card, amount)
      except PaymentError as e:      # also catches InsufficientFunds
          retry_later(e.payment_id)
      
      Show answer

      Answer Inheritance

      Deciding clue
      InsufficientFunds subclasses PaymentError and inherits its fields and behavior.
      Why
      It’s a kind of PaymentError, and code that catches the parent catches the child. That substitutability is what inheritance is for.
      Tempting wrong turn
      Dependency is the relationship between charge()’s caller and these exceptions, not between the two exception classes.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    99. 99AdminUser → UserCode · TypeScript · Document editor

      Admins can edit any document.

      TypeScript
      1
      2
      3
      4
      5
      6
      7
      8
      
      class User {
        constructor(public id: string, public email: string) {}
        canEdit(doc: Doc): boolean { return doc.ownerId === this.id; }
      }
      
      class AdminUser extends User {
        override canEdit(_doc: Doc): boolean { return true; }
      }
      
      Show answer

      Answer Inheritance

      Deciding clue
      extends User and overrides a method.
      Why
      An admin is a kind of user that behaves differently in one place. A subclass with an override is textbook generalization.
      Tempting wrong turn
      Dependency applies to AdminUser → Doc, which is a parameter. Make sure you answer for the pair asked.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance
    100. 100Truck → VehicleCode · Python · Parking lot

      Vehicle types in a parking-lot design.

      Python
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      class Vehicle:
          SIZE = Size.MEDIUM
          def __init__(self, plate: str):
              self.plate = plate
      
      class Truck(Vehicle):
          SIZE = Size.LARGE
      
      class Motorcycle(Vehicle):
          SIZE = Size.SMALL
      
      Show answer

      Answer Inheritance

      Deciding clue
      Truck(Vehicle) subclasses Vehicle.
      Why
      The code expresses “a Truck is a Vehicle”. Interview follow-up: when subclasses differ only in a constant, a VehicleType enum field is often simpler. Reach for subclasses when behavior differs.
      Tempting wrong turn
      Composition (a type field) is the alternative design, not what this code does.

      The path through the tree

      1. Is A a kind of B, or does A implement B's contract? Yes, extends a class
      2. Inheritance

    How to tell them apart

    • Dependency or association? Ignore the class names and look for a field. If A stores B, it’s an association; if B appears only in a parameter, local or return type, it’s a dependency.
    • Composition or aggregation? “Has” doesn’t mean “owns”. It’s composition only if B dies with A and no other object shares it. If B is passed in, shared or survives A, it’s aggregation.
    • Association class? Look for data that belongs to the pairing rather than either side, such as a quantity, status, date or role. That data needs a class of its own, such as Enrollment or CartItem.
    • Inheritance or realization? Check what the parent is. Extending a class is inheritance; implementing an interface, protocol or trait is realization.
    • Two questions that look alike? Find the one fact that differs between them; that fact decides the answer.

    Patterns are arrangements of these relationships

    Most design patterns are fixed arrangements of the relationships from Part 1, which makes them much easier to learn once the relationships are second nature:

    Four patterns as relationship shapes: Strategy (Checkout holds a Pricing interface that Hourly implements), Observer (Ticker holds many Observers that EmailAlert implements), Composite (File and Folder are Nodes, and a Folder owns many Nodes), Decorator (LoggingStore is a Store and wraps a Store) Green boxes are interfaces or abstract types. Composite and Decorator share one trick: a class that both is a type and holds that same type.

    Strategy and Observer have the same shape; the differences are one holder versus many, and the intent. Seen this way, you can recognize a pattern in unfamiliar code just from how its types connect. Parts 3 to 5 draw every pattern this way.

    Part 3: Creational patterns

    Who gets to call new? Creating an object hides three decisions: how many to make, which concrete type, and how to put it together. Each creational pattern takes over one of them.

    Pattern Decides In one line
    Singleton How many? Exactly one, shared by everyone.
    Factory Which type? Decided in one place, returned as an interface.
    Builder How assembled? Step by step, checked once at the end.

    Reading the diagrams

    Each pattern gets a class diagram. The arrows are the relationships from Part 1, and this notation holds for the rest of the post.

    UML notation used in this post: class box compartments and seven arrow types Go has no classes, so read a class box as a struct or an interface. Exported (capitalized) names are +, unexported names are -.

    Singleton

    How many? Make exactly one instance, and give the whole program one way to reach it.

    • Reach for it when: A second copy would waste or corrupt something: a connection pool, a metrics registry, a cache, a loaded config.
    • Go idiom: sync.Once, or sync.OnceValue in Go 1.21+
    • Python idiom: A module-level object. Modules are already singletons.

    Class diagram

    In UML, a singleton is a class that keeps a reference to its only instance and exposes a static accessor. Go has no static members, so the “static” parts are package-level variables.

    Class diagram: Config singleton with package-level instance and once fields, a static Get method, and two clients that call Get Config keeps a reference to its only instance (the arrow that loops back to itself, multiplicity 1). Callers never construct a Config; they call Get(), which is underlined because it belongs to the package, not to an instance.

    The race it has to survive

    The obvious version checks for nil and then creates. Two goroutines can both pass the check before either one assigns, and now you have two objects. This is the most common singleton bug.

    Timeline comparison: the naive check-then-create lets two goroutines each create a Config; sync.Once makes the second goroutine wait and reuse the first one go test -race flags the naive version. sync.Once makes the second caller wait for the first, then skip the work entirely.

    Code

    config/config.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    package config
    
    import (
        "os"
        "sync"
    )
    
    type Config struct {
        DSN      string
        MaxConns int
    }
    
    var (
        instance *Config   // the one and only
        once     sync.Once // guards the first creation
    )
    
    // Get returns the shared Config. Safe to call from any goroutine.
    func Get() *Config {
        once.Do(func() {
            instance = &Config{
                DSN:      os.Getenv("DB_DSN"),
                MaxConns: 20,
            }
        })
        return instance
    }
    
    Python · config.py: the Pythonic way
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    # config.py
    # The Pythonic singleton is a module-level object. Python runs a module
    # once and caches it in sys.modules, so every import gets the same object.
    import os
    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class Config:
        dsn: str
        max_conns: int = 20
    
    settings = Config(dsn=os.environ.get("DB_DSN", ""))
    
    # anywhere else: `from config import settings` gets this same object
    

    Where you’ll find it: http.DefaultClient and slog.Default() (shared package-level defaults), prometheus.MustRegister (one global registry, which panics on a duplicate metric), and logging.getLogger("app") (one logger per name, process-wide).

    Traps

    Hidden dependencies. A function that calls config.Get() inside hides that dependency from its signature, and a test can’t swap it. Create the instance once in main and pass it down: still exactly one, with no global lookup.

    One per process, not one per system. Three replicas, four gunicorn workers or a multiprocessing pool each get their own copy. Shared state belongs in a database or Redis, and one actor across replicas needs leader election (client-go’s leaderelection with a Lease).

    A failed first initialization is cached. sync.Once never retries. Use sync.OnceValues so every caller at least gets the error, or load in main and exit on failure.

    The GIL doesn’t make check-then-create safe. A thread can be switched out between the is None check and the assignment. A class-based singleton needs a lock and a second check inside it; the module-level object needs neither.

    Factory

    Which type? Put the decision about which concrete type to create in one place, and hand back an interface.

    • Reach for it when: The concrete type depends on config or input (storage backend, cloud, payment provider), or the same switch on type shows up in several places.
    • Go idiom: func NewStore(kind string) (Store, error), returning an interface
    • Python idiom: A dict of classes, filled by a @register decorator

    Class diagram

    Class diagram: Uploader holds a Store interface and calls NewStore, which creates S3Store, GCSStore or LocalStore, all of which implement Store Uploader depends only on the Store interface. NewStore is the one place that knows the three concrete types exist: it creates them («create») and returns them as Store.

    Code

    storage/storage.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    package storage
    
    import (
        "context"
        "fmt"
    )
    
    // Store is all the caller depends on.
    type Store interface {
        Put(ctx context.Context, key string, data []byte) error
        Get(ctx context.Context, key string) ([]byte, error)
    }
    
    type S3Store struct{ bucket string }
    type GCSStore struct{ bucket string }
    type LocalStore struct{ root string }
    
    // (each type implements Put and Get; omitted here)
    
    // NewStore is the factory. The caller names what it wants;
    // this function decides which concrete type that means.
    func NewStore(kind, target string) (Store, error) {
        switch kind {
        case "s3":
            return &S3Store{bucket: target}, nil
        case "gcs":
            return &GCSStore{bucket: target}, nil
        case "local":
            return &LocalStore{root: target}, nil
        default:
            return nil, fmt.Errorf("unknown store kind %q", kind)
        }
    }
    
    Go · Using it
    1
    2
    3
    4
    5
    6
    
    // kind comes from config, so switching backends is a config change.
    store, err := storage.NewStore(cfg.Storage.Kind, cfg.Storage.Target)
    if err != nil {
        log.Fatalf("storage: %v", err) // fail at startup, not on the first upload
    }
    uploader := NewUploader(store) // Uploader only knows the Store interface
    
    Python · storage.py: a registry filled by a decorator
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    from typing import Callable, Protocol
    
    class Store(Protocol):
        def put(self, key: str, data: bytes) -> None: ...
        def get(self, key: str) -> bytes: ...
    
    _REGISTRY: dict[str, Callable[[str], Store]] = {}
    
    def register(kind: str):
        """Class decorator: adds the class to the factory under `kind`."""
        def wrap(cls):
            _REGISTRY[kind] = cls
            return cls
        return wrap
    
    @register("s3")
    class S3Store:
        def __init__(self, bucket: str):
            self.bucket = bucket
        def put(self, key, data): ...
        def get(self, key): ...
    
    def new_store(kind: str, target: str) -> Store:
        try:
            return _REGISTRY[kind](target)
        except KeyError:
            raise ValueError(f"unknown store kind {kind!r}") from None
    
    store = new_store("s3", "logs-bucket")   # an S3Store, typed as Store
    

    A Go detail worth remembering: the usual advice is “accept interfaces, return structs”, so a plain constructor like NewS3Store() returns *S3Store. A factory is the exception. Its whole job is that the caller doesn’t know which struct it gets, so it has to return the interface.

    Making it open: the registry factory

    A switch has to be edited for every new backend. In the registry version each backend adds itself, so the factory never changes. database/sql works exactly this way.

    storage/registry.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    package storage
    
    import (
        "fmt"
        "sync"
    )
    
    type Constructor func(target string) (Store, error)
    
    var (
        mu        sync.RWMutex
        factories = map[string]Constructor{}
    )
    
    // Register is called from each backend's init().
    func Register(kind string, c Constructor) {
        mu.Lock()
        defer mu.Unlock()
        if _, dup := factories[kind]; dup {
            panic("storage: Register called twice for " + kind)
        }
        factories[kind] = c
    }
    
    func NewStore(kind, target string) (Store, error) {
        mu.RLock()
        c, ok := factories[kind]
        mu.RUnlock()
        if !ok {
            return nil, fmt.Errorf("unknown store kind %q (missing import?)", kind)
        }
        return c(target)
    }
    
    Go · Each backend registers itself
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package s3store
    
    import "example.com/app/storage"
    
    type S3Store struct{ bucket string }
    
    // (S3Store implements Put and Get; omitted here)
    
    // Adds itself to the factory when the package is imported.
    func init() {
        storage.Register("s3", func(bucket string) (storage.Store, error) {
            return &S3Store{bucket: bucket}, nil
        })
    }
    
    // main.go
    import _ "example.com/app/storage/s3store" // imported only to run its init()
    

    That’s why import _ "github.com/lib/pq" exists: the blank import runs pq’s init(), which registers the “postgres” driver before main starts. sql.Open only looks the name up, and no connection opens until the first query. In Python, importing the module that carries the @register decorator does the same.

    Three things called “factory”

    People say “factory” for three different things. All three answer “which type?”, but they put the decision in different places.

      Simple factory Factory Method Abstract Factory
    What it is A function with a switch A method that subclasses override An object that makes a whole family
    Decides One type per call One type per subclass A matching set of types
    Go shape NewStore(kind) A func field you inject An interface with several New… methods

    Factory Method

    In a Factory Method, a base type writes the whole algorithm once and leaves one step, “create the thing I’ll work with”, to subclasses. Below, Exporter owns the batching; each subclass only decides which sink the batches go to.

    Class diagram: abstract Exporter defines export and an abstract create_sink; S3Exporter and CloudWatchExporter override create_sink to create S3Sink or CloudWatchSink Solid line with a hollow triangle means inherits. Exporter.export() calls create_sink(), which is abstract (italic). Each subclass overrides only that one method, and each creates its own sink.

    Go · Inject the factory method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // Go has no inheritance, so the "factory method" becomes a function
    // field you inject instead of a method you override.
    type Sink interface {
        Write(batch []Record) error
    }
    
    type Exporter struct {
        newSink func() Sink
    }
    
    func (e Exporter) Export(records []Record) error {
        sink := e.newSink() // the factory method
        for i := 0; i < len(records); i += 100 {
            if err := sink.Write(records[i:min(i+100, len(records))]); err != nil {
                return err
            }
        }
        return nil
    }
    
    s3 := Exporter{newSink: func() Sink { return &S3Sink{bucket: "logs"} }}
    cw := Exporter{newSink: func() Sink { return &CloudWatchSink{group: "/app"} }}
    
    Python · Override the factory method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    from abc import ABC, abstractmethod
    
    class Exporter(ABC):
        def export(self, records: list[dict]) -> None:
            sink = self.create_sink()            # the factory method
            for i in range(0, len(records), 100):
                sink.write(records[i:i + 100])   # batching is written once, here
    
        @abstractmethod
        def create_sink(self) -> "Sink": ...
    
    class S3Exporter(Exporter):
        def create_sink(self) -> "Sink":
            return S3Sink()                      # CloudWatchExporter returns a CloudWatchSink
    
    S3Exporter().export([{"msg": "hi"}] * 250)   # 3 batches to S3
    

    Abstract Factory

    When products come in families that must match, one factory object makes the whole family. A provisioner that works on both clouds is the classic case: a site on AWS needs an EC2 instance, an S3 bucket and an SQS queue, and none of them may come from Azure.

    Abstract factory as a grid: AWSFactory and AzureFactory each implement CloudFactory and create one row of products; each column implements the VM, Bucket or Queue interface Each row is one concrete factory; each column is one product interface. Choosing a factory chooses a whole row, so an EC2 VM writing to an Azure blob container can’t happen.

    cloud/factory.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    // One interface per product...
    type VM interface{ Start(ctx context.Context) error }
    type Bucket interface{ Put(ctx context.Context, key string, b []byte) error }
    type Queue interface{ Send(ctx context.Context, msg []byte) error }
    
    // ...and one factory interface that makes a matching family of them.
    type CloudFactory interface {
        NewVM(size string) VM
        NewBucket(name string) Bucket
        NewQueue(name string) Queue
    }
    
    type AWSFactory struct{ Region string }
    
    func (f AWSFactory) NewVM(size string) VM         { return &EC2Instance{region: f.Region, size: size} }
    func (f AWSFactory) NewBucket(name string) Bucket { return &S3Bucket{region: f.Region, name: name} }
    func (f AWSFactory) NewQueue(name string) Queue   { return &SQSQueue{region: f.Region, name: name} }
    
    // AzureFactory mirrors it with AzureVM, BlobContainer and ServiceBusQueue.
    
    // provisionSite never names a cloud, and everything it makes matches.
    func provisionSite(ctx context.Context, f CloudFactory) error {
        vm := f.NewVM("large")
        logs := f.NewBucket("site-logs")
        jobs := f.NewQueue("site-jobs")
        if err := logs.Put(ctx, "provisioned", nil); err != nil {
            return err
        }
        if err := jobs.Send(ctx, []byte("boot")); err != nil {
            return err
        }
        return vm.Start(ctx)
    }
    

    The factory itself is picked once at startup, usually by a simple factory that switches on "aws" or "azure". Adding a family, a third cloud, is cheap: one new factory. Adding a product, say NewDatabase, is the expensive direction: the interface and every concrete factory change.

    Where you’ll find it: sql.Open("pgx", dsn) and image.Decode (registries filled by init()), boto3.client("s3"), and SQLAlchemy’s create_engine(url), which picks the dialect from the URL scheme.

    Traps

    A factory for one type. That’s indirection with no decision in it. Add the factory when the second type shows up.

    Unknown kinds found late. Build the store in main and exit on error, so a typo in config fails the deploy instead of the first request.

    A switch that keeps growing. Move to a registry so new types add themselves, and the factory stops changing.

    Leaking the concrete type. If callers write store.(*S3Store), the interface isn’t doing its job. Add the method they need to Store, or rethink the split.

    Builder

    How assembled? Assemble a complex object one named step at a time, and validate it once at the end.

    • Reach for it when: A constructor needs many parameters, most of them optional, or some combinations are invalid.
    • Go idiom: A fluent builder for multi-step objects; functional options for constructors with defaults
    • Python idiom: Keyword arguments cover most cases; builders shine for step-by-step assembly like query builders

    The problem it removes

    Before: telescoping constructor

    Go
    1
    2
    
    p := NewPod("api", "nginx:1.27", 8080,
        nil, 3, true, "", 0)
    

    What is true? What does "" turn off? Swap the 3 and the 0 and it still compiles.

    After: builder

    Go
    1
    2
    3
    4
    5
    
    p, err := pod.New("api").
        Image("nginx:1.27").
        Port(8080).
        Replicas(3).
        Build()
    

    Every value has a name. Anything you skip keeps its default. Build() refuses a Pod with no image.

    Class diagram

    Class diagram: main calls New, then chains calls on PodBuilder, which holds a draft Pod and a list of errors; each setter returns the builder, and Build validates and creates the Pod PodBuilder collects settings and errors, and creates the Pod only in Build(). The textbook version adds a Director that runs a fixed recipe over a builder interface; in Go the calling code usually plays that role.

    What Build() reports

    What the builder below prints. Build() reports every problem at once with errors.Join, and returns a copy, so later builder calls don’t reach a Pod already built.

    Go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    p, err := pod.New("api").Image("nginx:1.27").Port(8080).Build()
    // p:   {Name:api Image:nginx:1.27 Ports:[8080] Env:map[] Replicas:1}
    // err: <nil>
    
    _, err = pod.New("api").Port(8080).Build()
    // err: image is required
    
    _, err = pod.New("api").Port(70000).Build()
    // err: port 70000 out of range
    //      image is required
    
    b := pod.New("api").Image("nginx:1.27")
    first, _ := b.Build()
    b.Env("LOG_LEVEL", "debug").Port(9090)
    second, _ := b.Build()
    // first:  {Name:api Image:nginx:1.27 Ports:[] Env:map[] Replicas:1}
    // second: {Name:api Image:nginx:1.27 Ports:[9090] Env:map[LOG_LEVEL:debug] Replicas:1}
    

    Code

    pod/pod.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    
    package pod
    
    import (
        "errors"
        "fmt"
        "maps"
        "slices"
    )
    
    type Pod struct {
        Name     string
        Image    string
        Ports    []int
        Env      map[string]string
        Replicas int
    }
    
    type PodBuilder struct {
        pod  Pod
        errs []error
    }
    
    func New(name string) *PodBuilder {
        return &PodBuilder{pod: Pod{Name: name, Replicas: 1, Env: map[string]string{}}}
    }
    
    func (b *PodBuilder) Image(ref string) *PodBuilder {
        b.pod.Image = ref
        return b // returning b is what lets calls chain
    }
    
    func (b *PodBuilder) Port(n int) *PodBuilder {
        if n < 1 || n > 65535 {
            b.errs = append(b.errs, fmt.Errorf("port %d out of range", n))
        }
        b.pod.Ports = append(b.pod.Ports, n)
        return b
    }
    
    func (b *PodBuilder) Env(key, value string) *PodBuilder {
        b.pod.Env[key] = value
        return b
    }
    
    func (b *PodBuilder) Replicas(n int) *PodBuilder {
        b.pod.Replicas = n
        return b
    }
    
    // Build checks everything once and hands back an independent copy.
    func (b *PodBuilder) Build() (Pod, error) {
        errs := slices.Clone(b.errs)
        if b.pod.Image == "" {
            errs = append(errs, errors.New("image is required"))
        }
        if err := errors.Join(errs...); err != nil {
            return Pod{}, err
        }
        p := b.pod
        p.Ports = slices.Clone(b.pod.Ports)
        p.Env = maps.Clone(b.pod.Env)
        return p, nil
    }
    
    The Go idiom: functional options

    For constructors that mostly need sensible defaults plus a few overrides, Go code usually reaches for functional options instead of a builder object. Each option is a small function that edits the struct being created.

    Go · server.go: functional options
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    type Server struct {
        addr    string
        timeout time.Duration
        tls     *tls.Config
    }
    
    type Option func(*Server)
    
    func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
    func WithTLS(c *tls.Config) Option       { return func(s *Server) { s.tls = c } }
    
    func NewServer(addr string, opts ...Option) *Server {
        s := &Server{addr: addr, timeout: 30 * time.Second} // defaults first
        for _, opt := range opts {
            opt(s) // then each option edits the struct
        }
        return s
    }
    
    srv := NewServer(":8443", WithTimeout(5*time.Second), WithTLS(tlsCfg))
    

    Pick functional options when the object is ready in one call and each option stands alone. Pick a fluent builder when assembly happens in steps, options depend on each other, or you want to collect several errors and report them together.

    Where you’ll find it: client-go apply configurations (corev1ac.Pod(…).WithSpec(…)), strings.Builder, SQLAlchemy’s select(…).where(…), and functional options in grpc.NewClient and the AWS SDK for Go v2.

    Traps

    A builder for a small object. Two required fields? A struct literal or a plain constructor is clearer. In Python, a dataclass with defaults and keyword arguments covers most cases.

    A Build() that checks nothing. Then the builder is setters with extra steps. The promise is “if you got a Pod back, it’s valid”.

    Shared slices and maps. Copy them in Build() with slices.Clone and maps.Clone. Otherwise reusing the builder quietly edits the Pod you already returned.

    Sharing a builder between goroutines. Builders are mutable and unsynchronized. Make one per construction.

    They combine

    Real libraries stack them. Spark’s session setup is a builder that ends in a singleton:

    Python · PySpark
    1
    2
    3
    4
    
    spark = (SparkSession.builder              # Builder: collect settings
             .appName("etl")
             .config("spark.sql.shuffle.partitions", "64")
             .getOrCreate())                   # Singleton: reuse the active session if there is one
    

    Likewise, sql.Open is a registry factory, and the *sql.DB it returns is meant to be opened once and shared: a singleton by convention.

    Check your understanding

    1. 1

      Every HTTP handler opens its own *sql.DB, and Postgres starts refusing connections. Which pattern fixes it?

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer Singleton

      You need one shared pool per process. In Go, open one *sql.DB in main, or behind sync.Once, and pass it to the handlers. A *sql.DB is already a pool and is safe for concurrent use.

    2. 2

      Config says storage.kind: s3 or local. The upload code shouldn’t care which.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer Factory

      The concrete type depends on config. NewStore(kind) decides once, in one place, and hands back the Store interface, so the uploader never names a concrete type.

    3. 3

      A load balancer config has 15 optional settings, and an HTTPS listener is invalid without a certificate.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer Builder

      Many optional fields plus a rule that spans fields. Collect the settings step by step, and enforce the cross-field rule in Build(), the one place that sees everything before the object exists.

    4. 4

      The provisioner creates a VM, a bucket and a queue, and all three must come from the same cloud.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer Abstract Factory

      A family of products that must match. Pick one factory, AWS or Azure, and every product comes from that row, so mixing clouds can’t happen.

    5. 5

      A base Exporter batches records. Each subclass only decides where the batches are written.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer Factory Method

      The algorithm, batching, is written once in the base type, and the one creation step, create_sink(), is left to subclasses. In Go the same idea is a func field you inject.

    6. 6

      Only one replica of your controller may act at a time, across a 3-replica Deployment.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer None of these

      A singleton is one per process, so three replicas means three instances. One actor across replicas needs leader election, such as client-go’s leaderelection with a Kubernetes Lease.

    7. 7

      A Point struct with two required fields, X and Y.

      1. Singleton
      2. Factory
      3. Factory Method
      4. Abstract Factory
      5. Builder
      6. None of these
      Show answer

      Answer None of these

      A struct literal, Point{X: 1, Y: 2}, is already clear. A builder or a factory here only adds ceremony.

    8. 8

      SparkSession.builder.appName("etl").getOrCreate() uses which two patterns?

      1. Factory + Builder
      2. Singleton + Abstract Factory
      3. Builder + Singleton
      4. Factory Method + Builder
      Show answer

      Answer Builder + Singleton

      The chain collects settings, which is a builder, and getOrCreate() returns the active session if one already exists, which is a singleton.

    9. 9

      A Go service whose config.Get() is guarded by sync.Once runs as a Deployment with 3 replicas. How many Config objects exist?

      1. One, shared by all replicas
      2. One per goroutine that calls Get()
      3. Three, one per replica
      4. It depends on GOMAXPROCS
      Show answer

      Answer Three, one per replica

      A singleton is one per process, and each replica is its own process with its own memory. Gunicorn workers and multiprocessing pools behave the same way. Shared state across replicas belongs in a database or Redis.

    10. 10

      The function passed to sync.Once fails on its first run because the config file is missing. What do later calls get?

      1. Once runs the function again until it succeeds
      2. Once panics on the next call
      3. Nothing new: the function never runs again, so they get the failed result
      4. They block until the file appears
      Show answer

      Answer Nothing new: the function never runs again, so they get the failed result

      sync.Once runs the function once, whatever happens. Use sync.OnceValues so every caller at least sees the error, or load the config in main and exit on failure.

    11. 11

      Two goroutines call a singleton getter that does if instance == nil { instance = load() }. What can go wrong?

      1. Both can see nil before either assigns, so two instances get created
      2. Nothing; a pointer assignment is atomic
      3. The second goroutine deadlocks
      4. Go panics on concurrent reads
      Show answer

      Answer Both can see nil before either assigns, so two instances get created

      Check-then-create is the most common singleton bug, and go test -race flags it. sync.Once makes the second caller wait for the first and then skip the work. In Python, the GIL doesn’t save you either: a thread can be switched out between the check and the assignment.

    12. 12

      What is the Pythonic way to get a singleton?

      1. A metaclass that caches instances
      2. A module-level object, such as settings = Config(...) in config.py
      3. A class with a @staticmethod get_instance()
      4. A global inside every function that needs it
      Show answer

      Answer A module-level object, such as settings = Config(...) in config.py

      Python runs a module once and caches it in sys.modules, so every import gets the same object. A class-based singleton needs a lock and a double check to be thread-safe.

    13. 13

      Go style says “accept interfaces, return structs”. Why does NewStore(kind) return the Store interface anyway?

      1. Interfaces are faster to return
      2. The compiler requires it for switch statements
      3. So the caller can type-assert to *S3Store
      4. Its whole job is that the caller doesn’t know which struct it gets
      Show answer

      Answer Its whole job is that the caller doesn’t know which struct it gets

      A plain constructor such as NewS3Store() returns *S3Store. A factory is the exception: hiding the concrete type is the point. If callers start writing store.(*S3Store), the interface isn’t doing its job.

    14. 14

      What does import _ "github.com/lib/pq" do?

      1. Opens a database connection at startup
      2. Runs pq’s init(), which registers the “postgres” driver with database/sql
      3. Makes pq the default driver for every sql.Open call
      4. Nothing; the blank identifier discards the package
      Show answer

      Answer Runs pq’s init(), which registers the “postgres” driver with database/sql

      A blank import runs the package for its side effects. database/sql is a registry factory: drivers add themselves by name, and sql.Open only looks the name up. No connection opens until the first query.

    15. 15

      Why does the Pod builder’s Build() copy its slices and maps with slices.Clone and maps.Clone?

      1. To make Build() safe to call from several goroutines
      2. Otherwise reusing the builder would quietly edit Pods it already returned
      3. Because Go can’t return a map from a function
      4. To validate the ports
      Show answer

      Answer Otherwise reusing the builder would quietly edit Pods it already returned

      A copied struct still shares its slices and maps with the original. Without the clones, b.Env(...) after a Build() would change the first Pod too. Builders are still not safe to share between goroutines; make one per construction.

    16. 16

      When is a fluent builder a better fit than Go’s functional options?

      1. When the object has two required fields
      2. When assembly happens in steps, options depend on each other, or you want to report several errors together
      3. Whenever the constructor has defaults
      4. Never; functional options replace builders in Go
      Show answer

      Answer When assembly happens in steps, options depend on each other, or you want to report several errors together

      Functional options suit objects that are ready in one call, where each option stands alone. A fluent builder can collect state across steps and check everything once in Build(), as the Pod builder does with errors.Join.

    17. 17

      You add a fourth product, NewDatabase, to an Abstract Factory with AWS and Azure implementations. What has to change?

      1. Only the callers that need a database
      2. Only a new DatabaseFactory
      3. Nothing; Abstract Factory is open for extension
      4. The CloudFactory interface and every concrete factory
      Show answer

      Answer The CloudFactory interface and every concrete factory

      Abstract Factory makes adding a family, such as a third cloud, cheap: one new factory. Adding a product is the expensive direction, because every family must learn to make it.

    Part 4: Structural patterns

    What’s standing in the middle? Most structural patterns put one object between the caller and another. They differ in what that middle object changes: the shape of the call, the behavior around it, whether it gets through, or how much of the system you see. The shape to watch for on a diagram is a type that implements an interface and also holds a field of that same interface.

    Pattern Changes In one line
    Adapter The shape Makes one object fit an interface it wasn’t built for.
    Decorator The behavior Same interface, extra work before or after each call.
    Proxy The access Same interface, decides whether and when calls get through.
    Facade The surface One simple front door to a whole subsystem.
    Composite The count One item or a whole tree, same call.
    Bridge The axes A type that varies two ways becomes two hierarchies joined by a field.
    Flyweight The memory Many objects share one copy of identical state.

    Four wrappers compared: Adapter changes the call, Decorator keeps the call and adds behavior, Proxy keeps the call but may answer itself, Facade turns one call into calls on four subsystems Read each row left to right. Adapter: the call leaving the middle is a different call. Decorator: the same call, maybe several times. Proxy: the same call, or none at all when it can answer itself. Facade: one call in, four out.

    Adapter

    Changes the shape. Make an existing object usable through an interface it wasn’t written for, without changing either side.

    • Reach for it when: You integrate a vendor SDK, legacy code or a second provider, and you don’t want their API spread through your code.
    • Go idiom: A small struct that holds the foreign type and implements your interface
    • Python idiom: A class that wraps the foreign object and exposes your method names

    Class diagram

    Class diagram: AlertService holds a Notifier; SlackNotifier and TeamsNotifier implement Notifier and each hold a different vendor client This is the object adapter: it holds the vendor client in a field. The class adapter variant inherits from the adaptee instead; Go can’t do that, and in Python it’s rarely worth it.

    Code

    alert/slack.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    package alert
    
    import (
        "context"
        "fmt"
    
        "github.com/slack-go/slack"
    )
    
    type Severity int
    
    const (
        Info Severity = iota
        Critical
    )
    
    type Alert struct {
        Title    string
        Body     string
        Severity Severity
    }
    
    // Notifier is our interface, defined by the code that uses it.
    type Notifier interface {
        Notify(ctx context.Context, a Alert) error
    }
    
    // SlackNotifier adapts *slack.Client to Notifier.
    type SlackNotifier struct {
        client  *slack.Client
        channel string
    }
    
    func (s *SlackNotifier) Notify(ctx context.Context, a Alert) error {
        text := a.Title + "\n" + a.Body
        if a.Severity == Critical {
            text = ":rotating_light: " + text
        }
        _, _, err := s.client.PostMessageContext(ctx, s.channel, slack.MsgOptionText(text, false))
        if err != nil {
            return fmt.Errorf("slack notify: %w", err) // don't leak the vendor's error types
        }
        return nil
    }
    
    // Compile-time check that SlackNotifier implements Notifier.
    var _ Notifier = (*SlackNotifier)(nil)
    
    Go · The adapter in net/http
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    // The standard library ships an adapter you use every day:
    //
    //   type HandlerFunc func(ResponseWriter, *Request)
    //   func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
    //
    // It turns a plain function into an http.Handler.
    func health(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    }
    
    http.Handle("/healthz", http.HandlerFunc(health))
    

    In Go, define the interface where it’s used, with only the methods that code needs. Then the adapter stays tiny, and tests can swap in a fake Notifier without touching Slack.

    Where you’ll find it: http.HandlerFunc (Go’s docs call it an adapter), strings.NewReader, Python’s io.TextIOWrapper, dockershim (CRI to Docker, until Kubernetes 1.24), and any MCP server in front of a REST API.

    Traps

    Business logic creeping in. Keep the adapter to translation. If it starts deciding who gets paged, move that rule into your service, where every vendor benefits.

    Vendor types leaking out. Return your own errors and types. Wrap with %w so callers can still inspect the cause if they must.

    An interface shaped like the first vendor. If Notifier mirrors Slack’s options, the PagerDuty adapter gets awkward. Design the interface from what your code needs.

    Wrapping the whole SDK. Adapt only the calls you use. A three-method interface is easy to adapt and easy to fake.

    Decorator

    Adds behavior. Wrap an object in another with the same interface, adding behavior before or after each call. Stack as many as you like.

    • Reach for it when: You need logging, retries, metrics, caching, timeouts or compression around existing code, in combinations that change.
    • Go idiom: Middleware: func(http.Handler) http.Handler; or a struct with a next field
    • Python idiom: The @decorator syntax, which is this pattern applied to functions

    Class diagram

    The shape to remember: a decorator is a Sender (it implements the interface) and has a Sender (its next field). Because the wrapped thing has the same type as the wrapper, wrappers nest without limit.

    Class diagram: EmailSender and SenderDecorator implement Sender; SenderDecorator also holds a next Sender; RetrySender and LoggingSender extend SenderDecorator Dashed line with a hollow triangle: implements. Solid line with a hollow triangle: extends. The accent arrow is the next field, pointing back at the interface.

    Order is part of the design

    The same three layers in two orders, with the first SMTP attempt failing. Indentation shows nesting.

    Retry inside Logging: Logging(Metrics(Retry(email)))

    Text
    1
    2
    3
    4
    5
    6
    7
    8
    
    logging → Send(order-1042)
        retry: attempt 1
          email: SMTP timeout
        retry: wait 100ms
        retry: attempt 2
          email: 250 OK
      metrics: send_total{result="ok"} +1
    logging ← nil
    

    1 log line, 1 metric increment, 2 SMTP attempts.

    Retry outside Logging: Retry(Logging(Metrics(email)))

    Text
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    retry: attempt 1
      logging → Send(order-1042)
          email: SMTP timeout
        metrics: send_total{result="error"} +1
      logging ← err: SMTP timeout
    retry: wait 100ms
    retry: attempt 2
      logging → Send(order-1042)
          email: 250 OK
        metrics: send_total{result="ok"} +1
      logging ← nil
    

    2 log lines, 2 metric increments, 2 SMTP attempts. With Retry inside Logging you get one log line per call; with Retry outside, one per attempt. Order matters elsewhere too: with Auth outside Logging, rejected requests never get logged.

    Code

    In Go the same shape is usually HTTP middleware: a function that takes an http.Handler and returns one.

    web/middleware.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    package web
    
    import (
        "log/slog"
        "net/http"
        "time"
    )
    
    // Logging is a decorator: it is an http.Handler, and it wraps one.
    func Logging(log *slog.Logger) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()
                next.ServeHTTP(w, r) // the wrapped handler does the real work
                log.Info("request", "method", r.Method, "path", r.URL.Path, "took", time.Since(start))
            })
        }
    }
    
    // Wrappers stack, and the outermost runs first:
    //   http.Handle("/orders", Logging(logger)(Auth(orders)))
    
    decorators.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    import functools
    import logging
    import time
    
    from tenacity import retry, stop_after_attempt
    
    def logged(fn):
        @functools.wraps(fn)                 # keep fn's name and docstring
        def inner(*args, **kwargs):
            start = time.perf_counter()
            try:
                return fn(*args, **kwargs)
            finally:
                logging.info("%s took %.1f ms", fn.__name__, (time.perf_counter() - start) * 1000)
        return inner
    
    @logged                                  # outer layer: listed first, runs first
    @retry(stop=stop_after_attempt(3))       # inner layer
    def send_email(to: str, body: str) -> None:
        ...
    

    Where you’ll find it: HTTP middleware and http.TimeoutHandler, gzip.NewReader and bufio.NewReader, otelhttp.NewTransport, gRPC interceptors, and Python’s @functools.lru_cache.

    Traps

    Optional interfaces disappear. Wrapping http.ResponseWriter hides http.Flusher and http.Hijacker, so streaming and websockets quietly break behind your middleware. Give the wrapper an Unwrap() http.ResponseWriter method so http.ResponseController can reach the original.

    Identity checks break. A wrapped value is a different value. == comparisons and type assertions against the concrete type fail.

    Forgetting functools.wraps. Without it the wrapper replaces the function’s name and docstring, which confuses logs, debuggers and frameworks that register things by name.

    Proxy

    Controls access. Stand in for another object behind the same interface, and decide whether, when and how calls reach it.

    • Reach for it when: The real object is expensive to create, lives on another machine, needs protecting, or keeps getting asked the same question.
    • Go idiom: A struct with the same interface that holds, or lazily builds, the real one; httputil.ReverseProxy
    • Python idiom: A class with the same methods; Django’s SimpleLazyObject

    Proxies are named for what they do with a call. A virtual proxy builds the real object on first use, a caching proxy answers repeats itself, a protection proxy refuses unauthorized calls, and a remote proxy forwards over the network. One proxy can be several; the one below is virtual and caching.

    Class diagram

    Class diagram: CachingCatalog and RemoteCatalog both implement Catalog; the proxy holds the real catalog, created lazily, plus a cache Structurally the same as a decorator: implements Catalog and holds a Catalog. The difference is that this one owns its target and may not forward at all.

    Code

    catalog/proxy.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    
    package catalog
    
    import (
        "context"
        "sync"
    )
    
    type Item struct {
        ID    string
        Price int
    }
    
    type Catalog interface {
        Get(ctx context.Context, id string) (Item, error)
    }
    
    // CachingCatalog is a proxy: the same interface as the real catalog,
    // but it decides whether a call reaches the real one at all.
    type CachingCatalog struct {
        newReal func() (Catalog, error) // virtual proxy: build the real one on first use
    
        once    sync.Once
        real    Catalog
        realErr error
    
        mu    sync.Mutex
        cache map[string]Item
    }
    
    func NewCachingCatalog(newReal func() (Catalog, error)) *CachingCatalog {
        return &CachingCatalog{newReal: newReal, cache: map[string]Item{}}
    }
    
    func (p *CachingCatalog) Get(ctx context.Context, id string) (Item, error) {
        p.mu.Lock()
        it, ok := p.cache[id]
        p.mu.Unlock()
        if ok {
            return it, nil // answered without touching the real catalog
        }
    
        backend, err := p.backend()
        if err != nil {
            return Item{}, err
        }
        it, err = backend.Get(ctx, id) // the slow network call happens outside the lock
        if err != nil {
            return Item{}, err
        }
    
        p.mu.Lock()
        p.cache[id] = it
        p.mu.Unlock()
        return it, nil
    }
    
    func (p *CachingCatalog) backend() (Catalog, error) {
        p.once.Do(func() { p.real, p.realErr = p.newReal() })
        return p.real, p.realErr
    }
    

    The rest of the diagram is the real catalog, and the Checkout that can’t tell it from the proxy:

    Go · The real catalog and its client
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    // catalog/remote.go
    // RemoteCatalog is the real object: every Get is a network call.
    type RemoteCatalog struct {
        conn *grpc.ClientConn
    }
    
    func NewRemoteCatalog(conn *grpc.ClientConn) *RemoteCatalog {
        return &RemoteCatalog{conn: conn}
    }
    
    // (RemoteCatalog implements Get with an RPC over conn; omitted here)
    
    // checkout/checkout.go
    // Checkout depends only on the Catalog interface.
    type Checkout struct {
        catalog catalog.Catalog
    }
    
    func New(c catalog.Catalog) *Checkout {
        return &Checkout{catalog: c}
    }
    
    func (c *Checkout) Price(ctx context.Context, id string) (int, error) {
        it, err := c.catalog.Get(ctx, id)
        return it.Price, err
    }
    
    // main.go: Checkout gets the proxy; the remote catalog is created on first use.
    proxy := catalog.NewCachingCatalog(func() (catalog.Catalog, error) {
        conn, err := grpc.NewClient("catalog:443",
            grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
            return nil, err
        }
        return catalog.NewRemoteCatalog(conn), nil
    })
    co := checkout.New(proxy)
    

    Where you’ll find it: httputil.ReverseProxy, Envoy and NGINX (remote), gRPC client stubs (remote), and Django’s request.user or SQLAlchemy lazy relationships (virtual).

    Traps

    Holding a lock across the remote call. Every caller then waits behind the slowest request. Check the cache under the lock, call outside it, and store under the lock again, as the code does.

    A stampede on a cold cache. A hundred concurrent misses for one key make a hundred backend calls. golang.org/x/sync/singleflight collapses them into one.

    Stale answers. A cache needs a TTL or an invalidation rule. Decide which before you ship.

    A failed lazy init stays failed. sync.Once never retries, as in the Singleton traps. If creating the real object can fail transiently, use a mutex and retry.

    Facade

    Shrinks the surface. Give a complicated subsystem one simple front door, so most callers never have to learn the parts behind it.

    • Reach for it when: Several callers repeat the same multi-step sequence across subsystems, or using a module means learning too many of its parts.
    • Go idiom: A struct with a few high-level methods that holds the subsystem clients
    • Python idiom: A class or module-level function; a package’s __init__.py re-exporting the simple path

    Class diagram

    Class diagram: SitesHandler calls the SiteProvisioner facade, which holds the Network, Compute, DNS and Registry subsystems; callers can still reach a subsystem directly The facade holds the subsystems and offers two methods. It doesn’t hide them: the dashed path shows an advanced caller going straight to Registry.

    Code

    An infrastructure facade has a job a textbook one doesn’t: when step three fails, steps one and two have already changed the world. This one records an undo for each finished step and runs them in reverse on failure.

    sites/provisioner.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    
    package sites
    
    import (
        "context"
        "fmt"
    )
    
    type Spec struct {
        Name, VPC, CIDR, Size string
        Nodes                 int
    }
    
    type Site struct {
        Name   string
        Subnet string
        Nodes  []string
    }
    
    // The subsystems, each with its own detailed API.
    type Network interface {
        CreateSubnet(ctx context.Context, vpcID, cidr string) (string, error)
        DeleteSubnet(ctx context.Context, subnetID string) error
    }
    type Compute interface {
        LaunchNodes(ctx context.Context, subnetID, size string, n int) ([]string, error)
        TerminateNodes(ctx context.Context, ids []string) error
    }
    type DNS interface {
        UpsertRecord(ctx context.Context, name string, targets []string) error
    }
    type Registry interface {
        Save(ctx context.Context, s Site) error
    }
    
    // SiteProvisioner is the facade: one call instead of four services.
    type SiteProvisioner struct {
        net Network
        vm  Compute
        dns DNS
        reg Registry
    }
    
    func (p *SiteProvisioner) Provision(ctx context.Context, spec Spec) (site Site, err error) {
        var undo []func() // compensating steps, run in reverse if a later step fails
        defer func() {
            if err != nil {
                for i := len(undo) - 1; i >= 0; i-- {
                    undo[i]()
                }
            }
        }()
    
        subnet, err := p.net.CreateSubnet(ctx, spec.VPC, spec.CIDR)
        if err != nil {
            return Site{}, fmt.Errorf("create subnet: %w", err)
        }
        undo = append(undo, func() { _ = p.net.DeleteSubnet(context.WithoutCancel(ctx), subnet) })
    
        nodes, err := p.vm.LaunchNodes(ctx, subnet, spec.Size, spec.Nodes)
        if err != nil {
            return Site{}, fmt.Errorf("launch nodes: %w", err)
        }
        undo = append(undo, func() { _ = p.vm.TerminateNodes(context.WithoutCancel(ctx), nodes) })
    
        site = Site{Name: spec.Name + ".sites.example.com", Subnet: subnet, Nodes: nodes}
        if err = p.dns.UpsertRecord(ctx, site.Name, nodes); err != nil {
            return Site{}, fmt.Errorf("dns record: %w", err)
        }
        if err = p.reg.Save(ctx, site); err != nil {
            return Site{}, fmt.Errorf("register: %w", err)
        }
        return site, nil
    }
    

    The undo steps use context.WithoutCancel(ctx), so cleanup still runs when the original request was cancelled, which is often the very reason a step failed.

    Where you’ll find it: os.ReadFile, requests.get, boto3’s upload_file (multipart upload, parallel parts and per-part retries behind one call), kubectl apply, and Terraform modules.

    Traps

    The god facade. Sixty methods means it has become a second copy of the subsystem. Split facades by use case.

    Partial failure. Decide what happens when step three fails: undo in reverse, as above, or make every step idempotent so a retry is safe.

    Blocking the escape hatch. Most callers want the simple path; a few need the details. Leave the subsystems reachable.

    Composite

    Hides the count. Arrange objects in a tree and let callers treat a single item and a whole group exactly the same way.

    • Reach for it when: The data is naturally a tree (resources, UI widgets, org charts, rule sets) and you keep writing “if it’s a group, loop; otherwise…”.
    • Go idiom: One interface implemented by the leaf and by a group that holds []Interface
    • Python idiom: The same, with a children list; duck typing means no shared base is required

    Class diagram

    The giveaway shape: Group implements Resource and holds many Resources. That one aggregation arrow pointing back at the interface is what makes the structure recursive.

    Class diagram: Pod and Group both implement Resource; Group also holds many Resources as children, which is what makes the tree recursive * on the aggregation means a group holds any number of children, and each child may itself be a group. The hollow diamond fits the convention from Part 1: a pod can outlive any cost group it’s counted in.

    Code

    capacity/resource.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    package capacity
    
    type Resource interface {
        Name() string
        CPU() int // millicores requested
    }
    
    // Pod is a leaf.
    type Pod struct {
        name     string
        millicpu int
    }
    
    func (p Pod) Name() string { return p.name }
    func (p Pod) CPU() int     { return p.millicpu }
    
    // Group is the composite: it is a Resource, and it holds Resources.
    type Group struct {
        name     string
        children []Resource
    }
    
    func (g *Group) Name() string { return g.name }
    
    func (g *Group) CPU() int {
        total := 0
        for _, c := range g.children {
            total += c.CPU() // a child may be a Pod or another Group; no type switch
        }
        return total
    }
    
    func (g *Group) Add(rs ...Resource) *Group {
        g.children = append(g.children, rs...)
        return g
    }
    
    Go · Using it
    1
    2
    3
    4
    5
    
    payments := (&Group{name: "payments"}).Add(Pod{"api-1", 250}, Pod{"api-2", 250}, Pod{"worker", 300})
    search := (&Group{name: "search"}).Add(Pod{"indexer", 1000}, Pod{"query", 200})
    prod := (&Group{name: "prod"}).Add(payments, search)
    
    fmt.Println(prod.CPU(), payments.CPU(), Pod{"query", 200}.CPU()) // 2000 800 200
    

    Where you’ll find it: io.MultiWriter, errors.Join (errors.Is walks the whole tree), Kubernetes owner references, scikit-learn’s Pipeline, and Python’s ExceptionGroup.

    Traps

    Cycles. Adding a group to itself, or to its own descendant, makes CPU() recurse forever. Check in Add.

    Add on the shared interface. The “transparent” variant puts Add on Resource so callers never check types, but then Pod must implement Add and fail at runtime. The “safe” variant above keeps it on Group.

    Shared children. If one pod sits under two groups, totals count it twice. A composite assumes a tree, not a graph.

    Recomputing big trees. Summing 100,000 nodes on every request adds up. Cache totals on groups and invalidate on change.

    Bridge

    Splits two dimensions. When a type varies along two independent axes, give each axis its own hierarchy and connect them with a field instead of multiplying subclasses.

    • Reach for it when: You catch yourself naming types like IncidentSlack and DigestEmail: two ideas glued into one name. Three message kinds times three channels is nine such types; as a bridge it’s three plus three.
    • Where you’ll find it: log/slog: a Logger holds a Handler. database/sql: a DB holds a driver.Driver.
    notify/bridge.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // Implementation side: how a message gets delivered.
    type Sender interface {
        Deliver(ctx context.Context, to, subject, body string) error
    }
    
    // Abstraction side: what kind of message it is.
    // Each kind holds a Sender. That field is the bridge.
    type Incident struct {
        sender Sender
        id     string
        sev    int
    }
    
    func (m Incident) Send(ctx context.Context, oncall string) error {
        subject := fmt.Sprintf("[SEV%d] incident %s", m.sev, m.id)
        return m.sender.Deliver(ctx, oncall, subject, "See the runbook.")
    }
    
    type Digest struct {
        sender Sender
        items  []string
    }
    
    func (m Digest) Send(ctx context.Context, team string) error {
        return m.sender.Deliver(ctx, team, "Daily digest", strings.Join(m.items, "\n"))
    }
    
    // Any kind × any channel, picked at runtime. Adding Teams means one new Sender.
    err := Incident{sender: SlackSender{}, id: "INC-311", sev: 1}.Send(ctx, "#oncall")
    

    Flyweight

    Shares identical state. When you have huge numbers of similar objects, share the parts that are identical and store only the differences per object.

    • Vocabulary: Intrinsic state is shared and immutable (the label set). Extrinsic state is per object (time and value).
    • Where you’ll find it: Prometheus keeps each series’ label set once and appends only (timestamp, value) pairs. CPython caches small ints and interns many strings. Go 1.23 added unique.Make.
    • Python idiom: An @lru_cache factory that returns frozen dataclasses; sys.intern for strings
    metrics/labels.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    type Labels struct {
        Service, Region, Host string
    }
    
    // LabelPool is the flyweight factory: one shared *Labels per distinct value.
    type LabelPool struct {
        mu   sync.Mutex
        sets map[Labels]*Labels
    }
    
    func (p *LabelPool) Get(l Labels) *Labels {
        p.mu.Lock()
        defer p.mu.Unlock()
        if shared, ok := p.sets[l]; ok {
            return shared
        }
        if p.sets == nil {
            p.sets = map[Labels]*Labels{}
        }
        shared := &l
        p.sets[l] = shared
        return shared
    }
    
    // Each sample stores only what differs, plus a pointer to the shared part.
    type Sample struct {
        labels *Labels // shared: treat as read-only
        ts     int64
        value  float64
    }
    
    Go · package unique, Go 1.23+
    1
    2
    3
    4
    5
    6
    
    // Go 1.23+: package unique interns comparable values for you,
    // and frees them once nothing refers to them any more.
    a := unique.Make(Labels{Service: "api", Region: "us-east-1", Host: "ip-10-0-1-7"})
    b := unique.Make(Labels{Service: "api", Region: "us-east-1", Host: "ip-10-0-1-7"})
    fmt.Println(a == b) // true: one shared copy, compared by pointer
    labels := a.Value() // read the shared value
    

    Traps

    Mutable shared state. If one sample edits its labels, every sample sharing them changes. The shared part must be immutable.

    A pool that never forgets. An interning map that only grows is a memory leak. unique.Make frees values nothing refers to; a plain map doesn’t.

    They combine

    A few lines of ordinary Go server code hold three of these patterns:

    main.go
    1
    2
    3
    4
    5
    6
    7
    
    backend, err := url.Parse("http://orders.internal:8080")
    if err != nil {
        log.Fatal(err)
    }
    rp := httputil.NewSingleHostReverseProxy(backend) // Proxy: the real handler is on another machine
    http.Handle("/orders/", Logging(logger)(rp))      // Decorator: same http.Handler, more behavior
    // ...and inside Logging, http.HandlerFunc is an Adapter.
    

    Check your understanding

    1. 1

      Your code calls Notify(ctx, alert); the vendor SDK only offers PostMessageContext(ctx, channel, opts...).

      1. Facade
      2. Adapter
      3. Bridge
      4. Proxy
      Show answer

      Answer Adapter

      One object behind it, and a different shape on each side. The adapter translates between your interface and the SDK’s, and nothing upstream learns about channels or message options.

    2. 2

      Add request logging and panic recovery to every HTTP handler without editing any of them.

      1. Decorator
      2. Proxy
      3. Facade
      4. Composite
      Show answer

      Answer Decorator

      The same http.Handler interface, extra behavior around each call, and the wrappers stack. Middleware is the Decorator pattern.

    3. 3

      The catalog lives in another region. Answer repeat lookups from memory, and don’t dial it at all until the first miss.

      1. Decorator
      2. Adapter
      3. Proxy
      4. Flyweight
      Show answer

      Answer Proxy

      Same interface, and it decides whether and when calls reach the real object: a caching proxy that is also a virtual proxy. A decorator would always forward.

    4. 4

      Five callers each create a subnet, launch nodes, write a DNS record and register the site, in that order.

      1. Adapter
      2. Composite
      3. Bridge
      4. Facade
      Show answer

      Answer Facade

      One simple front door over four subsystems, so the sequence, and what to undo when a step fails, is written once.

    5. 5

      Report CPU for one pod, a namespace or the whole cluster with the same call.

      1. Composite
      2. Decorator
      3. Facade
      4. Flyweight
      Show answer

      Answer Composite

      One item or a whole tree behind the same interface. Groups sum their children and never check whether a child is a pod or another group.

    6. 6

      Three alert kinds times three delivery channels, and both lists keep growing.

      1. Adapter
      2. Decorator
      3. Bridge
      4. Facade
      Show answer

      Answer Bridge

      Two independent axes. Give each its own hierarchy and connect them with a field, so a new channel is one new type instead of three.

    7. 7

      Ten million metric samples, but only a few thousand distinct label sets.

      1. Proxy
      2. Composite
      3. Bridge
      4. Flyweight
      Show answer

      Answer Flyweight

      Share the identical, immutable part, the label set, and store only each sample’s time and value.

    8. 8

      http.HandlerFunc(health)

      1. Decorator
      2. Adapter
      3. Proxy
      4. Facade
      Show answer

      Answer Adapter

      It makes a plain function satisfy http.Handler without changing the function or the interface. Go’s own documentation calls it an adapter.

    9. 9

      gzip.NewReader(file) returns an io.Reader that decompresses as you read.

      1. Adapter
      2. Proxy
      3. Facade
      4. Decorator
      Show answer

      Answer Decorator

      An io.Reader wrapping an io.Reader and adding behavior. You can stack bufio.NewReader on top of it.

    10. 10

      httputil.NewSingleHostReverseProxy(backend)

      1. Proxy
      2. Adapter
      3. Decorator
      4. Facade
      Show answer

      Answer Proxy

      An http.Handler standing in for a handler on another machine: a remote proxy.

    11. 11

      Logging(Metrics(Retry(email))) sends one message, and the first SMTP attempt fails. How many log lines and metric increments do you get?

      1. 1 log line, 1 metric increment
      2. 2 log lines, 2 metric increments
      3. 1 log line, 2 metric increments
      4. 2 log lines, 1 metric increment
      Show answer

      Answer 1 log line, 1 metric increment

      Retry is the innermost layer, so both attempts happen inside one pass through Metrics and Logging. Put Retry outside, Retry(Logging(Metrics(email))), and every attempt is logged and counted: 2 and 2.

    12. 12

      A middleware wraps http.ResponseWriter to record the status code. What can quietly break behind it?

      1. Request logging
      2. Streaming and websockets, because the wrapper hides http.Flusher and http.Hijacker
      3. Query-string parsing
      4. Nothing; the wrapper has the same interface
      Show answer

      Answer Streaming and websockets, because the wrapper hides http.Flusher and http.Hijacker

      Optional interfaces disappear behind a decorator that doesn’t implement them. Give the wrapper an Unwrap() http.ResponseWriter method so http.ResponseController can reach the original.

    13. 13

      Why does CachingCatalog.Get release its mutex before calling the real catalog?

      1. Go mutexes can’t be held across function calls
      2. To make the cache eventually consistent
      3. So callers don’t all wait behind the slowest network request
      4. So two callers can never fetch the same key
      Show answer

      Answer So callers don’t all wait behind the slowest network request

      Check the cache under the lock, call outside it, and store under the lock again. The cost is that concurrent misses for one key each call the backend; singleflight collapses those into one call.

    14. 14

      A hundred requests miss the cache for the same key at the same moment. What collapses them into one backend call?

      1. A longer TTL
      2. sync.Once
      3. A bigger mutex
      4. golang.org/x/sync/singleflight
      Show answer

      Answer golang.org/x/sync/singleflight

      singleflight lets the first caller for a key do the work while the others wait and share its result. A TTL doesn’t help a cold cache, and sync.Once would never refresh the key.

    15. 15

      SiteProvisioner.Provision has created a subnet and launched nodes, and then the DNS step fails. What does the facade in this post do?

      1. Returns the error and leaves the subnet and nodes for an operator
      2. Retries the DNS step forever
      3. Terminates the nodes, then deletes the subnet, and returns the error
      4. Deletes the subnet first, then terminates the nodes
      Show answer

      Answer Terminates the nodes, then deletes the subnet, and returns the error

      Each finished step records an undo, and a deferred function runs them in reverse order on failure. The undo steps use context.WithoutCancel(ctx), so cleanup still runs when the request itself was cancelled.

    16. 16

      On a class diagram, a decorator and a proxy look identical. What separates them?

      1. A proxy always has a cache
      2. Intent: a decorator always forwards and adds behavior; a proxy may answer, refuse or delay, and often creates its target
      3. A decorator must be a function, a proxy a struct
      4. A proxy changes the interface
      Show answer

      Answer Intent: a decorator always forwards and adds behavior; a proxy may answer, refuse or delay, and often creates its target

      Both implement an interface and hold one value of it. A decorator is handed its target and stacks freely, where the order of layers matters. A proxy usually stands alone in front of one object and controls access to it.

    17. 17

      One pod is added under two cost groups, and their parent sums them. What goes wrong?

      1. CPU() recurses forever
      2. The pod’s CPU is counted twice
      3. The second Add fails
      4. Nothing; the pod reports the same CPU either way
      Show answer

      Answer The pod’s CPU is counted twice

      A composite assumes a tree, not a graph. A cycle, a group added to its own descendant, is the case that recurses forever; check for it in Add.

    Part 5: Behavioral patterns

    Who decides what happens next? Each behavioral pattern takes one decision away from code that would otherwise hard-wire it. Find the decision being moved and you’ve found the pattern.

    Pattern Decides In one line
    Strategy How the work is done Pick the algorithm at runtime; the caller stays the same.
    Observer Who reacts Everyone who subscribed hears it; the source doesn’t know who they are.
    State What a call means now Same call, different behavior per mode; the object switches itself.
    Command When it runs A call becomes a value you can queue, log, retry or undo.
    Chain Who handles it Each handler takes the request or hands it to the next.

    Five specialists follow: Template Method, Iterator, Mediator, Memento and Visitor.

    Strategy

    Swap the how. Define a family of interchangeable ways to do one job, and let the caller pick one, even while running.

    • Reach for it when: There are several ways to do one job (scaling policies, backoff schedules, compression, pricing rules) and the choice comes from config or changes at runtime.
    • Go idiom: A one-method interface, or simply a func value
    • Python idiom: A callable passed in (key=), or a small class per strategy

    Three policies, three situations

    Three scaling policies answer the same question: how many replicas should run? The autoscaler clamps every answer to 2–20.

    Situation TargetTracking{Target: 60} StepScaling{} Scheduled{Peak: 8, OffPeak: 2}
    CPU 84%, 4 replicas, 22:00 6 6 2
    CPU 45%, 4 replicas, 11:00 3 4 8
    CPU 20%, 6 replicas, 02:00 2 5 2

    Same inputs, three answers. Which one runs is a single SetPolicy call; the autoscaler’s own code never changes.

    Class diagram

    Class diagram: Autoscaler holds a ScalePolicy; TargetTracking, StepScaling and Scheduled implement it The autoscaler depends only on the ScalePolicy interface. Adding a fourth policy means writing one type; the autoscaler doesn’t change.

    Code

    scaling/policy.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    
    package scaling
    
    import "math"
    
    type Metrics struct {
        CPU  float64 // average utilization, 0–100
        Hour int     // local hour, 0–23
    }
    
    // ScalePolicy is the strategy: given the current size and the metrics,
    // how many replicas should run?
    type ScalePolicy interface {
        Desired(current int, m Metrics) int
    }
    
    type TargetTracking struct{ Target float64 }
    
    func (p TargetTracking) Desired(current int, m Metrics) int {
        return int(math.Ceil(float64(current) * m.CPU / p.Target))
    }
    
    type StepScaling struct{}
    
    func (StepScaling) Desired(current int, m Metrics) int {
        switch {
        case m.CPU > 80:
            return current + 2
        case m.CPU > 60:
            return current + 1
        case m.CPU < 30:
            return current - 1
        }
        return current
    }
    
    type Scheduled struct{ Peak, OffPeak int }
    
    func (p Scheduled) Desired(_ int, m Metrics) int {
        if m.Hour >= 9 && m.Hour < 18 {
            return p.Peak
        }
        return p.OffPeak
    }
    
    // Autoscaler is the context: it uses a policy without knowing which one.
    type Autoscaler struct {
        policy   ScalePolicy
        min, max int
    }
    
    func (a *Autoscaler) SetPolicy(p ScalePolicy) { a.policy = p } // swap at runtime
    
    func (a *Autoscaler) Decide(current int, m Metrics) int {
        return max(a.min, min(a.max, a.policy.Desired(current, m)))
    }
    
    Go · A func is a strategy too
    1
    2
    3
    4
    
    // A strategy doesn't have to be a type. Often it's just a function value.
    slices.SortFunc(pods, func(a, b Pod) int {
        return cmp.Compare(b.CPU, a.CPU) // the comparison strategy: busiest first
    })
    

    Where you’ll find it: slices.SortFunc (the comparison func is the strategy), http.Client.CheckRedirect, kube-scheduler plugins, Python’s sorted(key=…), and scikit-learn estimators.

    Traps

    Choosing in every caller. The switch that picks a strategy belongs in one place, typically a factory at startup, not spread through the code.

    Swapping while running. SetPolicy on one goroutine while another calls Decide is a data race. Guard the field with a mutex or keep it in an atomic.Pointer.

    A strategy for two branches that never change. An if is fine. Reach for the pattern when the options grow or come from config.

    Observer

    Broadcast a change. Let any number of objects subscribe to changes in another, and notify all of them when it changes, without the source knowing who they are.

    • Reach for it when: One change should trigger several independent reactions (metering, alerts, audit), and you want to add reactions without editing the source.
    • Go idiom: A registry of callbacks, or one buffered channel per subscriber
    • Python idiom: A list of callables; logging handlers; libraries such as blinker

    Delivery mode matters when one subscriber is slow. With synchronous callbacks, Publish returns only after the slowest one finishes, so the publisher waits. With one buffered channel per subscriber, Publish returns at once and each subscriber catches up from its buffer.

    Class diagram

    Class diagram: StatusFeed holds any number of Subscribers; Metering, Alerter and AuditLog implement Subscriber; CEController publishes to the feed The hollow diamond with * is the Gang of Four book’s way of saying the feed holds any number of subscribers it doesn’t own. Under the convention from Part 1, that’s a plain association: a feed isn’t made of its subscribers. The arrow points from the subject to the interface, never to a concrete subscriber.

    Code

    events/feed.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    package events
    
    import "sync"
    
    type Event struct {
        Engine string
        Status string // "Starting", "Running", "Degraded", ...
    }
    
    // StatusFeed is the subject. It knows its subscribers only as functions.
    type StatusFeed struct {
        mu     sync.Mutex
        nextID int
        subs   map[int]func(Event)
    }
    
    // Subscribe registers fn and returns a function that unsubscribes it.
    func (f *StatusFeed) Subscribe(fn func(Event)) (unsubscribe func()) {
        f.mu.Lock()
        defer f.mu.Unlock()
        if f.subs == nil {
            f.subs = map[int]func(Event){}
        }
        id := f.nextID
        f.nextID++
        f.subs[id] = fn
        return func() {
            f.mu.Lock()
            defer f.mu.Unlock()
            delete(f.subs, id)
        }
    }
    
    // Publish calls every subscriber. It copies the list first and calls the
    // callbacks outside the lock, so a callback may subscribe or unsubscribe
    // without deadlocking.
    func (f *StatusFeed) Publish(e Event) {
        f.mu.Lock()
        subs := make([]func(Event), 0, len(f.subs))
        for _, fn := range f.subs {
            subs = append(subs, fn)
        }
        f.mu.Unlock()
        for _, fn := range subs {
            fn(e)
        }
    }
    
    Go · The channel version
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    // The channel version: each subscriber gets its own buffered channel,
    // so one slow subscriber can't stall the publisher.
    type Broadcaster struct {
        mu   sync.Mutex
        subs []chan Event
    }
    
    func (b *Broadcaster) Subscribe(buffer int) <-chan Event {
        ch := make(chan Event, buffer)
        b.mu.Lock()
        b.subs = append(b.subs, ch)
        b.mu.Unlock()
        return ch
    }
    
    func (b *Broadcaster) Publish(e Event) (dropped int) {
        b.mu.Lock()
        defer b.mu.Unlock()
        for _, ch := range b.subs {
            select {
            case ch <- e:
            default:
                dropped++ // buffer full: drop instead of blocking (a deliberate policy)
            }
        }
        return dropped
    }
    

    Where you’ll find it: client-go informers’ AddEventHandler, signal.Notify, SNS, EventBridge and Kafka consumer groups across processes, Python logging handlers, and Django signals.

    Traps

    A full buffer needs a policy. When a channel subscriber falls behind, decide whether Publish blocks, drops the event (as Broadcaster does), or drops the oldest.

    Lapsed listeners. A subscriber that never unsubscribes stays alive forever, along with everything it references. Return an unsubscribe function and defer it.

    Relying on order. Go map iteration is random, so subscribers run in a different order each time. If B must run after A, that’s a workflow, not an observer.

    Calling back under a lock. If Publish holds the mutex while calling subscribers, one that calls Subscribe deadlocks. Copy the list, unlock, then call, as the code does.

    State

    Behave by mode. Let an object change its behavior when its internal mode changes, by giving each mode its own object.

    • Reach for it when: Every method starts with switch status, and adding a mode means editing all of them.
    • Go idiom: One type per state behind an interface, or a transition table for simple machines
    • Python idiom: A class per state with refusing defaults; or the transitions library

    The state machine

    Start() and Stop() are calls your code makes; done, timeout and crash are events from outside.

    UML state machine: Stopped goes to Starting on Start; Starting goes to Running when done or to Failed on timeout; Running goes to Stopping on Stop or Failed on crash; Stopping returns to Stopped when drained; Failed goes back to Starting on Start Rounded boxes are states, arrows are transitions labeled with what triggers them, and the black dot is where a new instance starts.

    State Start() Stop() Done() timeout crash
    Stopped → Starting no-op error: nothing in flight – –
    Starting no-op error: cannot stop while starting → Running → Failed –
    Running no-op → Stopping error: nothing in flight – → Failed
    Stopping error: wait until stopped no-op → Stopped – –
    Failed → Starting no-op error: nothing in flight – –

    A dash means the state doesn’t listen for that event.

    Class diagram

    Class diagram: Instance holds a current State; Stopped, Starting, Running and Stopping implement State and call back into Instance to set the next state Same shape as Strategy: the context holds an interface. The difference is the accent arrow. States call back into the instance to choose the next state.

    Code

    lifecycle/state.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    package lifecycle
    
    import "fmt"
    
    // State is one mode of an Instance. Each method decides what that call
    // means in this mode, and which state comes next.
    type State interface {
        Name() string
        Start(i *Instance) error
        Stop(i *Instance) error
        Done(i *Instance) error // the in-flight operation finished
    }
    
    type Instance struct{ state State }
    
    func NewInstance() *Instance { return &Instance{state: Stopped{}} }
    
    func (i *Instance) Start() error     { return i.state.Start(i) }
    func (i *Instance) Stop() error      { return i.state.Stop(i) }
    func (i *Instance) Done() error      { return i.state.Done(i) }
    func (i *Instance) State() string    { return i.state.Name() }
    func (i *Instance) setState(s State) { i.state = s }
    
    type Stopped struct{}
    
    func (Stopped) Name() string            { return "Stopped" }
    func (Stopped) Start(i *Instance) error { i.setState(Starting{}); return nil }
    func (Stopped) Stop(*Instance) error    { return nil } // already stopped
    func (Stopped) Done(*Instance) error    { return fmt.Errorf("nothing in flight") }
    
    type Starting struct{}
    
    func (Starting) Name() string           { return "Starting" }
    func (Starting) Start(*Instance) error  { return nil } // already on its way
    func (Starting) Stop(*Instance) error   { return fmt.Errorf("cannot stop while starting") }
    func (Starting) Done(i *Instance) error { i.setState(Running{}); return nil }
    
    type Running struct{}
    
    func (Running) Name() string           { return "Running" }
    func (Running) Start(*Instance) error  { return nil } // no-op: already running
    func (Running) Stop(i *Instance) error { i.setState(Stopping{}); return nil }
    func (Running) Done(*Instance) error   { return fmt.Errorf("nothing in flight") }
    
    type Stopping struct{}
    
    func (Stopping) Name() string           { return "Stopping" }
    func (Stopping) Start(*Instance) error  { return fmt.Errorf("wait until stopped") }
    func (Stopping) Stop(*Instance) error   { return nil } // already on its way
    func (Stopping) Done(i *Instance) error { i.setState(Stopped{}); return nil }
    
    Go · Using it
    1
    2
    3
    4
    5
    6
    
    i := NewInstance()
    _ = i.Start()          // Stopped → Starting
    _ = i.Done()           // Starting → Running
    _ = i.Start()          // Running: no-op
    fmt.Println(i.State()) // Running
    fmt.Println(i.Stop(), i.Stop(), i.Start()) // <nil> <nil> wait until stopped
    
    Or a transition table

    When states carry no data and no logic of their own, a table is shorter and easier to review. It also includes Failed, which the interface version leaves out for brevity.

    lifecycle/table.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    // Table-driven alternative: clearer when states carry no data or logic.
    type state string
    type event string
    
    var transitions = map[state]map[event]state{
        "Stopped":  {"start": "Starting"},
        "Starting": {"done": "Running", "timeout": "Failed"},
        "Running":  {"stop": "Stopping", "crash": "Failed"},
        "Stopping": {"done": "Stopped"},
        "Failed":   {"start": "Starting"},
    }
    
    func next(s state, e event) (state, error) {
        if to, ok := transitions[s][e]; ok {
            return to, nil
        }
        return s, fmt.Errorf("%s: event %q not allowed", s, e)
    }
    

    Where you’ll find it: EC2 instance and Kubernetes Pod lifecycles, TCP connection states, http.Server.ConnState, and AWS Step Functions.

    Traps

    Transitions scattered everywhere. If any code can set status = "Running", the machine is only a suggestion. Only states call setState.

    A state with no way out. Every in-progress state needs an exit, including a timeout. A Starting that never hears back must move to Failed, or it waits forever.

    Racing events. Two goroutines calling Start and Stop at once can both see Running. Lock the instance, or let one goroutine own it and feed it events through a channel.

    Silently ignored calls. Decide per state whether an unexpected call is a no-op or an error, and log it. A swallowed Stop() becomes a support ticket.

    Command

    Package the request. Turn a request into an object that carries everything needed to run it, so it can be queued, logged, retried, sent elsewhere or undone.

    • Reach for it when: You need to queue, schedule, retry, audit or undo operations, or hand them to another process.
    • Go idiom: An interface with Execute (and Undo); a func() closure for the simplest case
    • Python idiom: A dataclass per command; functools.partial for lightweight ones

    Class diagram

    Class diagram: Runner holds queued and executed Commands; Scale and RotateCert implement Command and act on the Cluster receiver The runner holds commands only through the interface. Both commands act on the same receiver, which is the code that actually changes the cluster.

    Code

    ops/command.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    
    package ops
    
    import (
        "context"
        "fmt"
    )
    
    // Cluster is the receiver: it knows how to do the actual work.
    type Cluster struct {
        replicas int
        cert     string
    }
    
    func (c *Cluster) Replicas() int        { return c.replicas }
    func (c *Cluster) SetReplicas(n int)    { c.replicas = n }
    func (c *Cluster) InstallCert(p string) { c.cert = p }
    
    // Command is a request turned into a value: it can wait in a queue,
    // be logged, be retried, or be undone.
    type Command interface {
        Name() string
        Execute(ctx context.Context) error
        Undo(ctx context.Context) error
    }
    
    type Scale struct {
        C    *Cluster
        To   int
        from int // remembered by Execute so Undo can put it back
    }
    
    func (s *Scale) Name() string { return fmt.Sprintf("scale to %d", s.To) }
    
    func (s *Scale) Execute(context.Context) error {
        s.from = s.C.Replicas()
        s.C.SetReplicas(s.To)
        return nil
    }
    
    func (s *Scale) Undo(context.Context) error {
        s.C.SetReplicas(s.from)
        return nil
    }
    
    // Runner is the invoker: it decides when commands run, and keeps history.
    type Runner struct {
        queue   []Command
        history []Command
    }
    
    func (r *Runner) Submit(c Command) { r.queue = append(r.queue, c) }
    
    func (r *Runner) RunNext(ctx context.Context) error {
        if len(r.queue) == 0 {
            return nil
        }
        c := r.queue[0]
        r.queue = r.queue[1:]
        if err := c.Execute(ctx); err != nil {
            return fmt.Errorf("%s: %w", c.Name(), err)
        }
        r.history = append(r.history, c)
        return nil
    }
    
    func (r *Runner) Undo(ctx context.Context) error {
        if len(r.history) == 0 {
            return fmt.Errorf("nothing to undo")
        }
        c := r.history[len(r.history)-1]
        r.history = r.history[:len(r.history)-1]
        return c.Undo(ctx)
    }
    
    Go · Using it
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    c := &Cluster{replicas: 3}
    var r Runner
    r.Submit(&Scale{C: c, To: 5})
    r.Submit(&Scale{C: c, To: 8})
    
    _ = r.RunNext(ctx) // 3 → 5
    _ = r.RunNext(ctx) // 5 → 8
    _ = r.Undo(ctx)    // back to 5
    
    fmt.Println(c.Replicas()) // 5
    

    Where you’ll find it: exec.Cmd (configure now, Run later), database migrations (up and down), terraform plan -out then apply, and SQS messages or Celery tasks run by another process.

    Traps

    Undo that can’t undo. An email can’t be unsent and a deleted volume can’t be undeleted. Mark those commands irreversible, or give them a compensating action.

    Capturing references, not values. A command holding a pointer to a mutable request sees later edits. Copy what it needs when it’s created.

    Retries without idempotency. A worker that crashes after Execute but before acknowledging runs the command twice. Give each command an ID and make Execute safe to repeat.

    Undo after someone else’s change. Restoring 3 replicas after another operator scaled to 10 silently clobbers their change. Check the current value first, or refuse.

    Chain of Responsibility

    Pass it along. Give a request to a line of handlers. Each one either handles it or passes it to the next, so the sender doesn’t need to know who will take it.

    • Reach for it when: Several handlers might apply, in a known order of preference: authentication methods, credential sources, routing rules, approval levels.
    • Go idiom: A slice of handlers tried in order; or a next field on each link
    • Python idiom: A list of callables, each returning None to pass

    Where each credential stops

    Different credentials sent through an API gateway’s authentication chain of client certificate, then service account token, then OIDC:

    Credential Where it stops Result
    client certificate x509, the first link ci-runner
    service account token ServiceAccount, after x509 passes system:serviceaccount:ci:deployer
    OIDC id_token OIDC, after two passes alice@example.com
    expired service account token ServiceAccount: mine, but expired 401; the chain stops here
    no credentials falls off the end 401, ErrUnauthorized

    Class diagram

    Class diagram: CertAuth, TokenAuth and OIDCAuth implement Authenticator and each hold the next Authenticator; the Gateway holds the first link The classic shape: every link implements the interface and holds the next one. The accent arrow is that next field, pointing back at the interface.

    Code

    auth/chain.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    package auth
    
    import (
        "errors"
        "net/http"
        "strings"
    )
    
    type User struct{ Name string }
    
    // Authenticator is one link. ok=false means "not mine, ask the next one".
    // An error means "mine, but invalid".
    type Authenticator interface {
        Authenticate(r *http.Request) (u User, ok bool, err error)
    }
    
    // Chain tries each link in order; the first that says ok wins.
    // Note that Chain is itself an Authenticator, so chains nest.
    type Chain []Authenticator
    
    var ErrUnauthorized = errors.New("unauthorized")
    
    func (c Chain) Authenticate(r *http.Request) (User, bool, error) {
        for _, a := range c {
            u, ok, err := a.Authenticate(r)
            if err != nil {
                return User{}, false, err // recognized but invalid: stop here
            }
            if ok {
                return u, true, nil
            }
        }
        return User{}, false, ErrUnauthorized // fell off the end
    }
    
    type ClientCert struct{}
    
    func (ClientCert) Authenticate(r *http.Request) (User, bool, error) {
        if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
            return User{}, false, nil // not mine
        }
        return User{Name: r.TLS.PeerCertificates[0].Subject.CommonName}, true, nil
    }
    
    type BearerToken struct {
        // Verify reports ok=false for tokens it doesn't recognize (another issuer)
        // and an error for tokens it recognizes but rejects (expired, bad signature).
        Verify func(token string) (u User, ok bool, err error)
    }
    
    func (b BearerToken) Authenticate(r *http.Request) (User, bool, error) {
        token, found := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
        if !found {
            return User{}, false, nil // no bearer token: not mine
        }
        return b.Verify(token)
    }
    
    Go · Using it
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    authn := Chain{
        ClientCert{},
        BearerToken{Verify: serviceAccounts.Verify},
        BearerToken{Verify: oidcIssuer.Verify},
    }
    
    u, ok, err := authn.Authenticate(r)
    if err != nil || !ok {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
        return
    }
    

    Where you’ll find it: the AWS SDK default credential chain (environment, config files, web identity, container, then instance metadata; boto3 works the same way), kube-apiserver authentication, exceptions climbing the call stack, and Python logging propagation.

    Traps

    Falling off the end. Always end the chain with a decision, a rejection or an anonymous user, so nothing is silently dropped.

    Order is policy. Put cheap and specific handlers first. Moving OIDC ahead of client certificates changes who gets authenticated as what.

    What “mine, but invalid” means. This chain stops with 401 on an expired token. kube-apiserver’s union authenticator keeps trying the remaining authenticators by default and reports the combined errors. Both are reasonable; choose on purpose.

    Who handled it? Log the handler’s name with the result, or debugging means reading every link.

    Five specialists

    You’ll mostly use these rather than write them. Each gets the code and the one thing to know.

    Template Method

    Write the algorithm once, in a fixed order, and let subclasses (or, in Go, the type you pass in) fill in the steps.

    • Where you’ll find it: sort.Sort and heap.Init with their interfaces; unittest.TestCase (setUp, the test, tearDown); threading.Thread.run.
    Go · sort.Sort: the template method in Go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // sort.Sort is a template method: the algorithm is fixed inside the
    // standard library, and you supply the three steps it calls.
    type byCPU []Pod
    
    func (p byCPU) Len() int           { return len(p) }
    func (p byCPU) Less(i, j int) bool { return p[i].CPU > p[j].CPU }
    func (p byCPU) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
    
    sort.Sort(byCPU(pods)) // busiest first
    
    reconcile.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    from abc import ABC, abstractmethod
    
    class Reconciler(ABC):
        def run(self) -> None:                   # the template method: a fixed order
            want, have = self.desired(), self.actual()
            for change in self.diff(want, have):
                self.apply(change)
            self.report(len(want))
    
        @abstractmethod
        def desired(self) -> set: ...            # steps each subclass must fill in
    
        @abstractmethod
        def actual(self) -> set: ...
    
        @abstractmethod
        def apply(self, change) -> None: ...
    
        def diff(self, want: set, have: set):    # a default step, overridable
            return [("create", x) for x in sorted(want - have)] + \
                   [("delete", x) for x in sorted(have - want)]
    
        def report(self, n: int) -> None:        # a hook: does nothing unless overridden
            pass
    

    Template Method or Strategy? Template Method varies one step inside a fixed skeleton, usually through inheritance. Strategy swaps the whole algorithm through composition. In Go you’ll mostly write the Strategy shape, because there’s no inheritance to lean on.

    Iterator

    Walk through a collection one item at a time without knowing how it’s stored or fetched.

    • Where you’ll find it: Go 1.23 range-over-func with iter.Seq; bufio.Scanner; sql.Rows.Next; AWS SDK paginators; every Python for loop and generator.
    Go · pods.go (Go 1.23+)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // AllPods hides pagination behind an iterator (Go 1.23 range-over-func).
    func AllPods(ctx context.Context, api PodLister) iter.Seq2[Pod, error] {
        return func(yield func(Pod, error) bool) {
            token := ""
            for {
                page, next, err := api.ListPods(ctx, token)
                if err != nil {
                    yield(Pod{}, err)
                    return
                }
                for _, p := range page {
                    if !yield(p, nil) {
                        return // the caller broke out of the loop
                    }
                }
                if next == "" {
                    return
                }
                token = next
            }
        }
    }
    
    for pod, err := range AllPods(ctx, api) {
        if err != nil {
            return err
        }
        fmt.Println(pod.Name)
    }
    
    pods.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    def all_pods(api):
        """A generator hides pagination: callers just loop."""
        token = None
        while True:
            page, token = api.list_pods(token)
            yield from page
            if token is None:
                return
    
    for pod in all_pods(api):
        print(pod["name"])
    

    Stopping early must clean up. In Go, return as soon as yield returns false so pages, rows or connections are released. In Python, put cleanup in a finally block; it runs when the generator is closed.

    Mediator

    Stop components from talking to each other directly. They report to one object, and it decides what happens next.

    • Where you’ll find it: Kubernetes controllers never call each other; they coordinate through objects in the API server. A CI pipeline definition decides that tests run after the build, so the build step doesn’t have to.
    pipeline.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // Pipeline is the mediator: components report to it, and only it
    // decides what happens next. Components never call each other.
    type Pipeline struct {
        test   *Tester
        deploy *Deployer
        notify *Notifier
    }
    
    func (p *Pipeline) Notify(from, event string) {
        switch {
        case event == "failed":
            p.notify.Send(from + " failed")
        case from == "build":
            p.test.Run()
        case from == "test":
            p.deploy.Rollout()
        case from == "deploy":
            p.notify.Send("released")
        }
    }
    
    // A component only knows the mediator:
    func (b *Builder) Finish(ok bool) {
        if ok {
            b.pipeline.Notify("build", "ok")
        } else {
            b.pipeline.Notify("build", "failed")
        }
    }
    

    Changing the order of steps now means editing one object instead of four. The mediator can become a god object, so keep only coordination in it; the real work stays in the components.

    Memento

    Capture an object’s state as an opaque snapshot, so it can be restored later without exposing its internals.

    • Where you’ll find it: SAVEPOINT and ROLLBACK TO SAVEPOINT in SQL; etcdctl snapshot save and snapshot restore; EBS and VM snapshots; editor undo history.
    config/snapshot.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    package config
    
    // Snapshot is the memento. Its fields are unexported, so code outside this
    // package can store and hand back a Snapshot but can't read or change it.
    type Snapshot struct {
        replicas int
        image    string
    }
    
    type Config struct {
        replicas int
        image    string
    }
    
    func (c *Config) Save() Snapshot      { return Snapshot{c.replicas, c.image} }
    func (c *Config) Restore(s Snapshot)  { c.replicas, c.image = s.replicas, s.image }
    func (c *Config) SetImage(img string) { c.image = img }
    
    Go · The caretaker
    1
    2
    3
    
    history := []config.Snapshot{cfg.Save()} // the caretaker holds snapshots, never looks inside
    cfg.SetImage("api:v3")                   // a risky change
    cfg.Restore(history[len(history)-1])     // roll back
    

    Snapshots cost memory, and shallow copies share data. Keep a bounded history or store diffs, and copy maps, slices and pointers deeply, or the snapshot changes along with the live object.

    Visitor

    Add new operations over a fixed set of types without changing those types.

    • Where you’ll find it: go/ast.Walk with an ast.Visitor (and ast.Inspect, its function form); Python’s ast.NodeVisitor; linters and formatters walking syntax trees.
    resources.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    type Visitor interface {
        VisitPod(*Pod)
        VisitService(*Service)
    }
    
    type Resource interface{ Accept(Visitor) }
    
    type Pod struct {
        Name string
        CPU  int // millicores
    }
    
    type Service struct {
        Name         string
        LoadBalancer bool
    }
    
    func (p *Pod) Accept(v Visitor)     { v.VisitPod(p) }     // double dispatch: the element
    func (s *Service) Accept(v Visitor) { v.VisitService(s) } // picks the visitor method
    
    // A new operation is a new visitor; Pod and Service don't change.
    type CostVisitor struct{ Monthly float64 }
    
    func (c *CostVisitor) VisitPod(p *Pod) { c.Monthly += float64(p.CPU) / 1000 * 25 } // illustrative rate
    func (c *CostVisitor) VisitService(s *Service) {
        if s.LoadBalancer {
            c.Monthly += 18 // illustrative rate
        }
    }
    

    A type switch is often simpler. Visitor makes adding an operation cheap (one new visitor) and adding a type expensive (every visitor changes). A type switch is the opposite, so reach for Visitor only when the types are fixed and the operations keep growing.

    Check your understanding

    1. 1

      The autoscaler should switch between target tracking and a fixed schedule without any change to its own code.

      1. State
      2. Strategy
      3. Template Method
      4. Command
      Show answer

      Answer Strategy

      Several ways to do one job, picked by the caller or config and swappable at runtime with SetPolicy.

    2. 2

      When a compute engine’s status changes, metering, alerts and the audit log all need to react.

      1. Observer
      2. Mediator
      3. Chain
      4. Command
      Show answer

      Answer Observer

      One change, many independent reactions, and the source shouldn’t know who’s listening.

    3. 3

      Start() should launch nodes when stopped, do nothing when running, and refuse while stopping.

      1. Strategy
      2. Command
      3. State
      4. Chain
      Show answer

      Answer State

      The same call behaves differently in each mode, and the object moves itself between modes.

    4. 4

      Queue cluster operations, run them on a worker, and let an operator undo the last one.

      1. Memento
      2. Observer
      3. Strategy
      4. Command
      Show answer

      Answer Command

      Requests become values that can wait, run later, and carry what they need to undo themselves.

    5. 5

      Try a client certificate, then a bearer token, then OIDC. The first that works wins.

      1. Chain
      2. Strategy
      3. Observer
      4. Template Method
      Show answer

      Answer Chain

      Handlers in a fixed order of preference, each taking the request or passing it on.

    6. 6

      sort.Sort(data) runs a fixed algorithm and calls your Len, Less and Swap.

      1. Strategy
      2. Template Method
      3. Iterator
      4. Visitor
      Show answer

      Answer Template Method

      The skeleton is fixed in the library, and you fill in the steps it calls.

    7. 7

      for pod, err := range AllPods(ctx, api) hides three pages of API calls.

      1. Visitor
      2. Chain
      3. Iterator
      4. Command
      Show answer

      Answer Iterator

      Walk a collection one item at a time without knowing how it’s stored or fetched.

    8. 8

      Build, test, deploy and notify steps stop calling each other and report to one pipeline object instead.

      1. Observer
      2. Chain
      3. Facade
      4. Mediator
      Show answer

      Answer Mediator

      A central coordinator holds the who-reacts-to-what logic, so each component only knows the hub.

    9. 9

      Snapshot an object before a risky change and roll back, without exposing its fields.

      1. Memento
      2. Command
      3. Prototype
      4. State
      Show answer

      Answer Memento

      An opaque saved state that the owner can restore and nobody else can read. In Go, unexported fields enforce that.

    10. 10

      Add a cost report and a lint check across Pod, Service and Ingress without touching those types.

      1. Iterator
      2. Strategy
      3. Visitor
      4. Template Method
      Show answer

      Answer Visitor

      New operations over a fixed set of types: each operation becomes a visitor.

    11. 11

      TargetTracking{Target: 60} sees 84% CPU on 4 replicas, and the autoscaler clamps to 2–20. How many replicas does it ask for?

      1. 5
      2. 6
      3. 7
      4. 4
      Show answer

      Answer 6

      ceil(4 × 84 / 60) is ceil(5.6), which is 6, inside the clamp. StepScaling happens to agree (84% is above 80%, so 4 + 2), while Scheduled at 22:00 says 2.

    12. 12

      One goroutine calls SetPolicy while another is inside Decide. What is that, and what fixes it?

      1. A data race; guard the field with a mutex or keep it in an atomic.Pointer
      2. Safe, because interface assignment is atomic
      3. A deadlock; call SetPolicy only from Decide
      4. Safe, because strategies are stateless
      Show answer

      Answer A data race; guard the field with a mutex or keep it in an atomic.Pointer

      An interface value is two words, and an unsynchronized write next to a read is a data race even if it usually works. Swapping strategies at runtime needs atomic access.

    13. 13

      Why does StatusFeed.Publish copy the subscriber list and call the callbacks after unlocking?

      1. It’s faster to iterate over a slice than a map
      2. So a callback can subscribe or unsubscribe without deadlocking
      3. To deliver events in subscription order
      4. So callbacks run in parallel
      Show answer

      Answer So a callback can subscribe or unsubscribe without deadlocking

      Subscribe takes the same mutex. Holding it while calling subscribers means one that calls Subscribe waits on itself forever. Copy, unlock, then call.

    14. 14

      Subscriber B must always run after subscriber A. What does the post recommend?

      1. Register B after A; Go keeps registration order
      2. Give subscribers priorities
      3. Treat it as a workflow, not an observer: call them in order explicitly
      4. Use buffered channels
      Show answer

      Answer Treat it as a workflow, not an observer: call them in order explicitly

      The Go feed stores subscribers in a map, and map iteration order is random. Observers should be independent; a required order is a sequence, which belongs in code that calls the steps in order.

    15. 15

      On a Running instance from the State chapter, what does fmt.Println(i.Stop(), i.Stop(), i.Start()) print?

      1. <nil> <nil> <nil>
      2. <nil> <nil> wait until stopped
      3. <nil> cannot stop while starting <nil>
      4. <nil> already stopping wait until stopped
      Show answer

      Answer <nil> <nil> wait until stopped

      The first Stop() moves Running to Stopping. A second Stop() while Stopping is a no-op. Start() while Stopping is refused with “wait until stopped”.

    16. 16

      A worker crashes after a command’s Execute succeeded but before it acknowledged the message. What happens, and what protects you?

      1. Nothing; the queue knows it ran
      2. The command is lost; use a bigger queue
      3. The queue undoes it automatically
      4. It runs again; give each command an ID and make Execute safe to repeat
      Show answer

      Answer It runs again; give each command an ID and make Execute safe to repeat

      At-least-once delivery means a retry after a crash. Idempotent commands turn a duplicate run into a no-op.

    17. 17

      In this post’s authentication chain, an expired service account token arrives. What happens?

      1. The chain tries OIDC next
      2. The ServiceAccount link recognizes it as its own but invalid, so the chain stops with a 401
      3. The request is let through as anonymous
      4. x509 accepts it
      Show answer

      Answer The ServiceAccount link recognizes it as its own but invalid, so the chain stops with a 401

      This chain treats “mine, but invalid” as final. kube-apiserver’s union authenticator instead keeps trying the remaining authenticators by default and reports the combined errors. Both are reasonable; choose on purpose.

    18. 18

      Strategy and State both hold an interface and delegate to it. Which question tells them apart?

      1. Who flips the switch: the caller, or the object itself?
      2. How many implementations are there?
      3. Is the interface a single method?
      4. Is it written in Go or Python?
      Show answer

      Answer Who flips the switch: the caller, or the object itself?

      In Strategy the caller picks, and strategies never mention each other. In State each state knows which one comes next and sets it on the context.

    19. 19

      When does Visitor pay off over a type switch?

      1. When new element types are added often
      2. When there are only two operations
      3. When the set of types is fixed and the operations keep growing
      4. When the elements are in a list rather than a tree
      Show answer

      Answer When the set of types is fixed and the operations keep growing

      Visitor makes adding an operation cheap, one new visitor, and adding a type expensive, since every visitor must learn it. A type switch is the opposite.

    Part 6: Field guide: which pattern, and when?

    Everything from Parts 3 to 5, arranged for the moment you have a real problem: find the problem, get a pattern, rule out its lookalike, and check whether something simpler would do. All 23 Gang of Four patterns are here, plus the simple factory.

    The map

    Bucket The question Write often Use sometimes Rarely build
    Creational · how objects are born Who gets to call new? Singleton, Factory, Builder Factory Method, Abstract Factory, Prototype –
    Structural · how objects are wired What’s standing in the middle? Adapter, Decorator, Proxy, Facade, Composite Bridge Flyweight
    Behavioral · how objects decide Who decides what happens next? Strategy, Observer, State, Command, Chain of Responsibility Template Method, Iterator, Mediator Memento, Visitor, Interpreter

    Pattern finder

    Find the situation closest to yours.

    Creating objects

    Situation Pattern Why
    Every handler opens its own database pool or client Singleton You need one shared instance per process. Create it once, in main or behind sync.Once, and share it.
    The concrete type depends on config: s3, gcs or local Factory One function maps the config value to a type and returns the interface. Callers never name the concrete type.
    The same switch on type is copied into several places Factory Move the switch into one function. A registry lets new types add themselves.
    A constructor takes eight arguments, most of them optional Builder Name each setting and default the rest. In Go, functional options are the usual form.
    Some field combinations are invalid and must be rejected Builder Build() is the one place to validate across fields before the object exists.
    Several resources must all come from the same cloud or backend Abstract Factory Choosing one factory chooses a whole matching family, so mixed providers can’t happen.
    A shared workflow differs only in what it creates Factory Method Keep the workflow in one place and inject, or override, only the creation step.
    I must change an object that came from a shared cache Prototype Clone it first (DeepCopy(), copy.deepcopy) so other readers of the cache aren’t affected.

    Connecting and wrapping

    Situation Pattern Why
    A vendor SDK doesn’t match the interface my code uses Adapter A thin type translates your calls into the SDK’s and keeps vendor types out of your code.
    Add logging, metrics or retries around existing calls Decorator A wrapper with the same interface adds the behavior and stacks with other wrappers.
    Run the same code around every HTTP handler or RPC Decorator That’s middleware: func(http.Handler) http.Handler, or gRPC interceptors.
    A dependency is slow or remote and asked the same thing repeatedly Proxy A caching proxy answers repeats itself and calls through only on a miss.
    Don’t create or connect until something is actually needed Proxy A virtual proxy builds the real object on first use, with sync.Once inside.
    Check permissions before calls reach the real object Proxy A protection proxy refuses the call before forwarding anything.
    Callers repeat the same multi-step sequence across several services Facade Write the sequence once behind one method, including what happens when a step fails.
    The same call must work on one item or a whole tree of them Composite Groups implement the item’s interface and forward to their children.
    Type names glue two ideas together: IncidentSlack, DigestEmail Bridge Split the two axes into two hierarchies and join them with a field.
    Millions of objects carry the same data Flyweight Share one immutable copy per distinct value and keep only the differences per object.

    Deciding and reacting

    Situation Pattern Why
    Several algorithms for one job, picked by config Strategy Put each algorithm behind one small interface or func, and let config choose.
    Several parts of the system must react when something changes Observer Publish the change. Each part subscribes and decides what it means for it.
    Every method starts with switch status State Give each status its own type, or table row, so the behavior for each mode lives together.
    Some calls are only legal in some lifecycle states State Each state decides what a call means and which state comes next. Illegal calls get a clear error.
    Operations should wait in a queue and run on a worker Command Make each operation a value with Execute. The queue and the worker don’t need to know what it does.
    Undo the last operation Command Each command records what it needs to reverse itself. Also consider Memento if you need to restore a whole object’s state rather than reverse one step.
    Audit or replay every change Command Commands are values you can log, store and run again.
    Try several handlers in order; the first that can, does Chain of Responsibility Each handler takes the request or passes it on, and the order is explicit.
    Fall back across credential sources or auth methods Chain of Responsibility This is how the AWS credential chain and kube-apiserver authentication work.
    Variants share the order of steps and differ in a few of them Template Method Fix the skeleton once. Let each variant supply its own steps.
    Callers shouldn’t have to deal with pagination or streaming Iterator Hide the paging behind an iterator or generator. Callers just loop.
    Components call each other in a tangle Mediator They report to one hub, and the hub owns the who-does-what-next logic.
    Snapshot state before a risky change and roll back if needed Memento Save an opaque snapshot, and restore it if the change goes wrong.
    Add new operations over a fixed set of types Visitor Each operation becomes a visitor. The types only accept it.
    Users write small rules or filters that must be evaluated Interpreter Parse them into a tree of nodes that evaluate themselves. Better still, embed CEL or JSONPath.

    Choosing in a bucket

    Pick the bucket, and its tree narrows it to one pattern in one or two questions. An editable version of all three is in design-patterns-decision-map.drawio, which opens in draw.io.

    Creating objects

    Creational decision tree: how many leads to Singleton; which concrete type leads to Factory, Factory Method or Abstract Factory; how it's put together leads to Builder or Prototype Factory, Factory Method and Abstract Factory all answer “which type?”. They differ in who decides: the input, the subclass, or the family.

    Connecting objects

    Decision tree: if the middle object has a different interface, it's an Adapter for one object or a Facade for many; if the same interface and it holds children, Composite; otherwise Decorator if it adds behavior, Proxy if it controls access Two questions sort the five common wrappers. Bridge and Flyweight aren’t wrappers, so they sit outside the tree.

    Deciding behavior

    Decision tree: how the work is done leads to Strategy or Template Method; what each mode does leads to State; who reacts leads to Observer, Chain or Mediator; when it runs leads to Command Four kinds of decision, seven patterns. Interpreter belongs with the specialists: it evaluates a small language users write.

    Spot it in UML

    Most patterns leave a recognizable shape in a class diagram or in Go struct definitions, so you can name them in an unfamiliar codebase from the types alone.

    Implements I and holds one I

    Pattern: Decorator, Proxy

    Class shape: a type implements an interface and also holds one value of that same interface Decorator if it always forwards and adds behavior. Proxy if it may answer, refuse or delay. Chain of Responsibility has this shape too, when the field is next and a link may stop.

    Implements I and holds many I

    Pattern: Composite

    Class shape: a type implements an interface and also holds many values of that same interface The aggregation arrow back to its own interface, with *, is the giveaway.

    Holds an interface the caller can swap

    Pattern: Strategy

    Class shape: a context holds an interface and delegates to whichever implementation is plugged in The context only delegates. The implementations never mention each other.

    Same shape, but implementations set the next one

    Pattern: State

    Class shape: like Strategy, but each implementation calls back into the context to set the next one The back-arrow from a state to the context is what separates State from Strategy.

    Implements your interface, holds a foreign type

    Pattern: Adapter

    Class shape: a type implements your interface and holds a different, foreign type Two different types on either side. If both sides share an interface, it’s not an adapter.

    Holds several different types, offers fewer methods

    Pattern: Facade

    Class shape: one type holds several different subsystem types and offers a few high-level methods Callers depend on one type instead of four. The subsystems stay reachable.

    A function returns an interface and creates concrete types

    Pattern: Factory

    Class shape: a function creates one of several concrete types and returns them as one interface The «create» arrows fan out from one place. Callers see only the interface.

    Holds a list of listeners and loops over them

    Pattern: Observer

    Class shape: a subject holds a list of listeners of one interface and calls each on change The subject points at the interface, never at a concrete subscriber.

    Package-level instance behind a once

    Pattern: Singleton

    Class shape: a package-level instance and a once guard, reached through one accessor In Go there’s no static keyword. Underlined members are package-level variables.

    Where they live in one service

    A control-plane service that provisions and scales compute for customers, with the pattern at each component:

    Where Pattern Why it fits there
    API gateway Chain of Responsibility Authenticators tried in order: client certificate, service account token, OIDC.
    API gateway Decorator Logging, recovery and tracing wrapped around every handler.
    Handlers Builder Turn a request into a validated Spec before anything runs.
    Handlers → job queue Command The operation becomes a value that can wait, be retried, audited and undone.
    Worker State The engine lifecycle allows only legal transitions.
    Worker Strategy The scaling policy comes from config and can change at runtime.
    Catalog client Proxy Caches lookups and dials the backend only on first use.
    Status feed Observer Metering, alerts and the audit log react to status changes independently.
    Notifier Adapter Slack or PagerDuty behind one Notify call.
    Site provisioner Facade One Provision call over network, compute, DNS and registry, with rollback.
    Cloud factory Abstract Factory AWS or Azure for the whole site, never mixed.
    Cloud SDK clients Adapter Each product hides its SDK behind your own interface.
    main() Singleton One config and one database pool, created once and passed down.
    main() Factory NewStore(kind) picks the storage backend from config.
    main() Bridge slog.New(handler): the logging API and the output format vary independently.
    Paged API reads Iterator Callers range over all pods; the pages stay hidden.

    Lookalikes

    Most wrong picks come from a lookalike with the same shape. Each pair is separated by one question.

    Pair Ask If yes If no
    Factory vs Strategy Is it choosing what to create, once? Factory: picks what to create Strategy: picks how to do a job, and can be swapped later
    Factory Method vs Template Method Is the step that varies the creation of an object? Factory Method: the varying step creates the product Template Method: the varying steps do work
    Abstract Factory vs Builder Are several different objects made that must match? Abstract Factory: a matching set, all at once Builder: one complex object, step by step
    Prototype vs Memento Will the copy be used as a new object? Prototype: the copy becomes a new object Memento: the copy restores the original later
    Singleton vs Flyweight Is there exactly one instance of the type? Singleton: one instance, process-wide Flyweight: one shared instance per distinct value
    Adapter vs Facade Does it wrap one object to fit an interface that already exists? Adapter: one object, existing interface Facade: many objects, a new smaller interface
    Adapter vs Bridge Are you retrofitting two pieces that already exist? Adapter: retrofitted after the fact Bridge: designed up front for two axes
    Decorator vs Proxy Does it always forward the call and add behavior? Decorator: always forwards, adds behavior Proxy: may answer, refuse or delay
    Decorator vs Chain of Responsibility Does every call reach the wrapped object? Decorator: always forwards Chain of Responsibility: a link may handle it and stop
    Decorator vs Composite Does it wrap exactly one object of its interface? Decorator: wraps one Composite: holds many children
    Strategy vs State Does the caller pick the implementation? Strategy: the caller picks State: the states pick the next state
    Observer vs Mediator Does each listener decide for itself how to react? Observer: listeners decide Mediator: the hub decides who does what
    Observer vs Chain of Responsibility Should every receiver get it? Observer: everyone hears it Chain of Responsibility: the first able handler takes it
    Command vs Strategy Is it what to do, to run later or undo? Command: what to do, possibly later Strategy: how to do it, now
    Facade vs Mediator Do calls only flow inward, from callers to subsystems? Facade: callers call it Mediator: components report to it, and it calls them

    Rules of thumb

    • Name the decision before the pattern. Ask which bucket question you’re answering: who creates it, what sits in the middle, or who decides. The pattern follows from the decision.
    • Wait for the second case. A pattern with one implementation is indirection without a payoff. Add the factory, strategy or adapter when the second type, policy or vendor shows up.
    • In Go, reach for the smallest shape. A func before a one-method interface, a small interface before a type hierarchy, and define interfaces where they’re used. Most Go patterns are a few lines.
    • Concurrency changes the details. Singletons need sync.Once, observers need a policy for slow subscribers, strategies swapped at runtime need atomic access, and state machines need one owner or a lock.

    Cheat sheet

    “Try first” is what to write before reaching for the pattern; reach for it only when that stops being enough.

    Pattern Question it answers Try first Go shape
    Singleton How many? Create it once in main and pass it down var getConfig = sync.OnceValue(loadConfig)
    Factory Which type? The constructor, until a second implementation exists func NewStore(kind, target string) (Store, error)
    Factory Method Which type, per variant? A constructor func field, which is the whole pattern in Go type Exporter struct{ newSink func() Sink }
    Abstract Factory Which family? A simple factory, if there’s only one kind of product type CloudFactory interface{ NewVM(size string) VM; NewBucket(name string) Bucket }
    Builder How assembled? A struct literal, keyword arguments or functional options pod.New("api").Image("nginx:1.27").Port(8080).Build()
    Prototype Copy an existing one? Building a fresh one, if that’s cheap p := cached.DeepCopy(); p.Labels["team"] = "infra"
    Adapter The calls don’t fit? Changing one side, if you own both type SlackNotifier struct{ client *slack.Client } // implements Notifier
    Decorator Add behavior around calls? Inline code, if one call site needs it func Logging(next http.Handler) http.Handler
    Proxy Should the call get through? A direct call, if it’s cheap, local and unrestricted type CachingCatalog struct{ real Catalog; cache map[string]Item } // implements Catalog
    Facade Too many parts to learn? Leaving the sequence with its one caller until it repeats func (p *SiteProvisioner) Provision(ctx context.Context, s Spec) (Site, error)
    Composite One thing or many? A slice and a loop, if nesting is one level deep type Group struct{ children []Resource } // implements Resource
    Bridge Two things varying at once? One hierarchy, until the second axis really varies type Incident struct{ sender Sender }
    Flyweight Millions of near-identical objects? Plain values, until a profile shows the duplication h := unique.Make(labels) // Go 1.23+
    Strategy How should the work be done? An if, for two options that never change type ScalePolicy interface{ Desired(cur int, m Metrics) int }
    Observer Who needs to know? A direct call, for one reaction you own unsubscribe := feed.Subscribe(func(e Event) { ... })
    State What does this call mean right now? An enum and a switch, for a few states with little logic func (Running) Stop(i *Instance) error { i.setState(Stopping{}); return nil }
    Command When, and how many times? A plain call or closure, if it runs once, now type Command interface{ Execute(ctx context.Context) error; Undo(ctx context.Context) error }
    Chain of Responsibility Who handles this? An if/else, for two fixed checks type Chain []Authenticator // tries each in order until one says ok
    Template Method Same steps, different details? Steps as func parameters or a small interface sort.Sort(byCPU(pods)) // you supply Len, Less, Swap
    Iterator Walk it without knowing its insides? Returning a slice, if it’s small and in memory for pod, err := range AllPods(ctx, api) { ... } // iter.Seq2
    Mediator Who talks to whom? Direct calls, for a few components with a simple flow func (p *Pipeline) Notify(from, event string)
    Memento Can I roll back? Re-reading the source of truth, such as the database or API server snap := cfg.Save(); /* risky change */; cfg.Restore(snap)
    Visitor New operations over the same types? A type switch, if types change more often than operations ast.Walk(v, file) // calls v.Visit(node) for every node
    Interpreter Do users write little rules? Embedding CEL, JSONPath or Rego, or a config struct sel, _ := labels.Parse("app=api,tier!=db"); sel.Matches(labels.Set(pod.Labels))

    Prototype and Interpreter

    The two patterns not covered earlier.

    Prototype

    Copy an existing one. Create new objects by cloning a configured original, then change only what differs. Use it when configuring from scratch is costly, or when you must not mutate a shared original, such as an object from a Kubernetes informer cache.

    Go
    1
    2
    
    p := cached.DeepCopy() // edit a copy, never the object other cache readers share
    p.Labels["team"] = "infra"
    

    Also http.Request.Clone(ctx) and tls.Config.Clone(); in Python, copy.deepcopy or dataclasses.replace. The trap is a shallow copy, which shares maps, slices and pointers with the original.

    Interpreter

    Evaluate a little language. Represent a small language as a tree of objects that evaluate themselves. Regular expressions, text/template, Kubernetes label selectors, CEL and PromQL are all interpreters.

    Go
    1
    2
    
    sel, _ := labels.Parse("app=api,tier!=db") // parse once into a tree
    sel.Matches(labels.Set(pod.Labels))        // evaluate it per object
    

    Before building one, embed an existing language (CEL, JSONPath, Rego) or use a config struct.

    Check your understanding

    1. 1

      Before editing a Pod from an informer cache, you call pod.DeepCopy().

      1. Flyweight
      2. Prototype
      3. Memento
      4. Singleton
      Show answer

      Answer Prototype

      You clone a configured object and change the copy, so the shared original that other readers of the cache see stays untouched.

    2. 2

      The AWS SDK tries environment variables, then config files, then instance metadata.

      1. Strategy
      2. Observer
      3. Decorator
      4. Chain of Responsibility
      Show answer

      Answer Chain of Responsibility

      Sources in a fixed order; the first one with credentials wins and the rest are never asked.

    3. 3

      otelhttp.NewTransport(http.DefaultTransport)

      1. Decorator
      2. Adapter
      3. Bridge
      4. Proxy
      Show answer

      Answer Decorator

      A RoundTripper wrapping a RoundTripper, always forwarding and adding tracing spans.

    4. 4

      labels.Parse("app=api,tier!=db") returns a selector you run against objects.

      1. Visitor
      2. Chain of Responsibility
      3. Composite
      4. Interpreter
      Show answer

      Answer Interpreter

      A tiny language parsed into a tree that evaluates itself against each object’s labels.

    5. 5

      terraform plan -out=tfplan, then terraform apply tfplan later.

      1. Strategy
      2. Memento
      3. Builder
      4. Command
      Show answer

      Answer Command

      The change set is saved as a value and executed later, exactly as planned.

    6. 6

      Stopping an EC2 instance is legal only while it’s running.

      1. Chain of Responsibility
      2. Command
      3. Strategy
      4. State
      Show answer

      Answer State

      What a call means depends on the current mode, and the instance moves between modes itself.

    7. 7

      Informers let several controllers react to Pod changes through AddEventHandler.

      1. Chain of Responsibility
      2. Mediator
      3. Observer
      4. Command
      Show answer

      Answer Observer

      Every registered handler hears every change, and the informer doesn’t know who they are.

    8. 8

      io.MultiWriter(file, os.Stdout) is itself an io.Writer.

      1. Observer
      2. Decorator
      3. Composite
      4. Facade
      Show answer

      Answer Composite

      A writer made of writers. Callers can’t tell one from many. A decorator would wrap exactly one.

    9. 9

      sql.Open("pgx", dsn) picks a driver that registered itself by name.

      1. Abstract Factory
      2. Singleton
      3. Factory
      4. Strategy
      Show answer

      Answer Factory

      A registry factory: the name maps to a constructor, and you get back the common interface.

    10. 10

      grpc.NewClient(target, grpc.WithTransportCredentials(creds))

      1. Decorator
      2. Factory
      3. Prototype
      4. Builder
      Show answer

      Answer Builder

      Functional options: Go’s usual way to assemble an object from named, optional settings.

    11. 11

      prometheus.MustRegister panics if the same metric is registered twice.

      1. Singleton
      2. Proxy
      3. Factory
      4. Flyweight
      Show answer

      Answer Singleton

      There’s one default registry for the whole process, and it already holds that metric.

    12. 12

      Pipeline steps report “build done” to a hub, and the hub decides that tests run next.

      1. Observer
      2. Facade
      3. Mediator
      4. Chain of Responsibility
      Show answer

      Answer Mediator

      With Observer, each listener decides how to react. Here the hub decides who does what next, and calls flow both ways: components report to it, and it calls them.

    13. 13

      You copy a config before a risky change, so you can put the same object back if the change goes wrong.

      1. Prototype
      2. Command
      3. Flyweight
      4. Memento
      Show answer

      Answer Memento

      The lookalike question: will the copy be used as a new object? No, it restores the original later, so it’s a memento. A prototype’s copy becomes a new object.

    14. 14

      A metrics library keeps one shared, immutable Labels value per distinct label set.

      1. Singleton
      2. Flyweight
      3. Prototype
      4. Proxy
      Show answer

      Answer Flyweight

      One shared instance per distinct value is Flyweight. Singleton is exactly one instance of a type.

    15. 15

      Your service has exactly one storage backend today. Should you put a factory in front of it?

      1. Yes, so the second backend is easy later
      2. Yes, because factories make code testable
      3. Only if the backend is remote
      4. Not yet: call its constructor, and add the factory when the second backend arrives
      Show answer

      Answer Not yet: call its constructor, and add the factory when the second backend arrives

      Wait for the second case. A pattern with one implementation is indirection without a payoff. Tests can already pass a fake through an interface without a factory.

    Open mail app

    Write in Gmail · Outlook