DEV Community

Cover image for What Varies? A Go-To Mind Map of Design Patterns & SOLID
Rakshyak Satpathy
Rakshyak Satpathy

Posted on

What Varies? A Go-To Mind Map of Design Patterns & SOLID

Design Patterns & LLD — Go-To Mind Map Notes

Source: Dive Into Design Patterns (Alexander Shvets / Refactoring.Guru, v2023)

Format: recall-first mind map — every node is Q → hook → when → tiny Go skeleton

Diagrams: Mermaid (mindmap / flowchart)

Language: Go (interfaces + composition; no classical inheritance trees)


How to use these notes (recall pattern)

flowchart TD
  C["Central problem: What varies?"]
  C --> CREATE["CREATE objects"]
  C --> STRUCTURE["STRUCTURE objects"]
  C --> BEHAVE["COMMUNICATE / assign duties"]
Enter fullscreen mode Exit fullscreen mode

Active recall loop (best retention):

flowchart LR
  Q[Trigger Q] --> Say[Say one-liner]
  Say --> Sketch[Sketch participants]
  Sketch --> Code[Write Go skeleton]
  Code --> Check[Check notes]
  Check --> Drill[Confusion pairs]
Enter fullscreen mode Exit fullscreen mode
  1. Cover the answer. Read only the Trigger Q.
  2. Say the one-liner out loud (Feynman).
  3. Sketch the participants from memory.
  4. Write 5–10 lines of Go from memory, then check.
  5. Drill confusion pairs (Decorator vs Proxy, Strategy vs State, etc.).

Atomic card shape (every pattern):
| Field | Purpose |
|-------|---------|
| Trigger Q | When does this fire in an LLD/interview? |
| One-liner | Intent in one breath |
| Hook | Real-world metaphor to lock memory |
| Structure | Participants (Go names) |
| Skeleton | Minimal idiomatic Go |
| Trap | Common mix-up / anti-pattern |


ROOT MAP

mindmap
  root((Design Patterns & LLD))
    0 Mindset
    1 OOP Foundations
    2 Relations UML ladder
    3 Design Principles
    4 SOLID
    5 Creational
      how objects are born
    6 Structural
      how objects are wired
    7 Behavioral
      how objects talk / change
    8 Decision tree
    9 LLD interview playbook
Enter fullscreen mode Exit fullscreen mode

0. Mindset

Trigger Q: Pattern vs algorithm?

Answer: Algorithm = recipe (exact steps). Pattern = blueprint (shape of solution; code differs per app).

Why learn: toolkit of proven designs + shared vocabulary ("use a Strategy here").

Levels of reuse (Gamma):

flowchart LR
  Classes["Classes / libraries"] --> Patterns["Design patterns"]
  Patterns --> Frameworks["Frameworks"]
Enter fullscreen mode Exit fullscreen mode

Good design aims for: code reuse + extensibility (change is constant) without rigid coupling.


1. OOP Foundations

1.1 Objects & classes

Hook: Class = blueprint; object = instance (Oscar the Cat).

State = fields; behavior = methods; members = both.

1.2 Four pillars

Pillar One-liner Go feel
Abstraction Model only what the context needs (flight sim Airplane ≠ booking Airplane) types that omit irrelevant detail
Encapsulation Hide internals; expose a small public surface unexported fields + methods
Inheritance Build new types on existing ones embedding (limited; prefer interfaces)
Polymorphism Same call, different runtime behavior interface satisfaction
// Polymorphism via interface — Go's native "program to an interface"
type Flyer interface {
    Fly(origin, dest string, passengers int) error
}

type Airport struct{}

func (a Airport) Depart(f Flyer) error {
    return f.Fly("DEL", "BLR", 180) // works for Airplane, Helicopter, …
}
Enter fullscreen mode Exit fullscreen mode

1.3 Relations between objects (strength ladder)

flowchart LR
  Dep[Dependency] --> Assoc[Association]
  Assoc --> Agg[Aggregation]
  Agg --> Comp[Composition]
  Dep -.->|"weak → strong"| Comp
Enter fullscreen mode Exit fullscreen mode
flowchart TB
  subgraph Type relations
    Inh[Inheritance — is-a]
    Impl[Implementation — can / fulfills contract]
  end
Enter fullscreen mode Exit fullscreen mode
Relation Meaning Memory
Dependency Uses briefly (param/local); change may break you "borrows"
Association Long-lived link (field / always reachable) "knows"
Aggregation Whole–part; parts can outlive whole "has (shared)"
Composition Whole owns part lifecycle "owns"
Inheritance Is-a; reuses interface + impl "is"
Implementation Fulfills a contract "can"

2. Design Principles (pre-SOLID)

Encapsulate What Varies

Q: Where will change hurt most?

Do: Pull varying logic into its own method/type so the stable core doesn't thrash.

Hook: Ship compartments — a mine floods one bay, not the hull.

func (o Order) Total() float64 {
    sum := 0.0
    for _, li := range o.Items {
        sum += li.Price * float64(li.Qty)
    }
    return sum * (1 + TaxRate(o.Country)) // variation isolated
}

func TaxRate(country string) float64 {
    switch country {
    case "US":
        return 0.07
    case "EU":
        return 0.20
    default:
        return 0
    }
}
Enter fullscreen mode Exit fullscreen mode

Program to an Interface, not an Implementation

Q: Can I swap the collaborator without editing callers?

Do: Depend on the methods you need, not the concrete type.

type Employee interface {
    DoWork()
}

type Company struct{}

func (c Company) RunDay(staff []Employee) {
    for _, e := range staff {
        e.DoWork() // not *Designer, *Dev — the interface
    }
}
Enter fullscreen mode Exit fullscreen mode

Favor Composition Over Inheritance

Q: Am I multiplying subclasses across dimensions (engine × cargo × nav)?

Do: "Has-a" + delegate. Runtime-swappable behaviors.

Trap: Inheritance = one dimension; multi-dimension → combinatorial explosion.

type Engine interface{ Torque() float64 }
type Navigator interface{ Route(to string) []string }

type Vehicle struct {
    Engine
    Navigator
}

func (v Vehicle) Drive(to string) {
    _ = v.Torque()
    _ = v.Route(to)
}
Enter fullscreen mode Exit fullscreen mode

3. SOLID

Letter Rule Recall phrase
S One reason to change "one job, one class"
O Open for extension, closed for modification "add types, don't edit old ones"
L Subtypes must be substitutable "don't surprise the caller"
I No fat interfaces "don't force unused methods"
D High-level depends on abstractions "details plug into policy"

SRP

// BAD: Employee manages data AND prints timesheets
// GOOD:
type Employee struct{ ID, Name string }
type TimesheetReporter struct{}
func (TimesheetReporter) Print(e Employee) { /* format may change alone */ }
Enter fullscreen mode Exit fullscreen mode

OCP (+ Strategy)

type Shipping interface{ Cost(order Order) float64 }

type Order struct{ Shipping Shipping }

func (o Order) ShippingCost() float64 { return o.Shipping.Cost(o) }
// new shipping = new type; Order untouched
Enter fullscreen mode Exit fullscreen mode

LSP checklist (Go interfaces)

  • Don't strengthen preconditions / weaken postconditions in implementations
  • Don't throw unexpected errors the contract didn't advertise
  • No "is this the concrete type?" branches that break substitution

ISP

// BAD: CloudProvider with 40 methods
// GOOD: split
type BlobStore interface{ Put(key string, b []byte) error }
type Queue interface{ Publish(topic string, msg []byte) error }
Enter fullscreen mode Exit fullscreen mode

DIP

type ReportStore interface {
    Load(id string) (Report, error)
    Save(Report) error
}

type BudgetReport struct{ Store ReportStore } // high-level depends on interface
// PostgresStore / FileStore implement ReportStore — details depend on abstraction
Enter fullscreen mode Exit fullscreen mode

5. CREATIONAL — "how are objects born?"

mindmap
  root((Creational))
    Factory Method
      subclass decides product
    Abstract Factory
      families of products
    Builder
      step-by-step complex object
    Prototype
      clone existing instance
    Singleton
      one shared instance
Enter fullscreen mode Exit fullscreen mode

Factory Method

AKA: Virtual Constructor

Trigger Q: I know the interface of a product, but not which concrete type until a subclass/config decides?

One-liner: Creator defines CreateProduct(); subclasses return concrete products.

Hook: Logistics app — Transport is Truck or Ship; Logistics.CreateTransport() deferred to RoadLogistics / SeaLogistics.

When: framework/library hooks; parallel product hierarchies; replace new Concrete sprinkled everywhere.

type Button interface{ Render() }

type Dialog interface {
    CreateButton() Button // factory method
    Render()
}

type WindowsButton struct{}
func (WindowsButton) Render() { /* native win btn */ }

type WindowsDialog struct{}
func (WindowsDialog) CreateButton() Button { return WindowsButton{} }
func (d WindowsDialog) Render() {
    btn := d.CreateButton()
    btn.Render()
}
Enter fullscreen mode Exit fullscreen mode

Trap: Not the same as Abstract Factory (one product vs families).

Relates: often grows into Abstract Factory; pairs with Template Method; Iterator may use it for iterators.


Abstract Factory

Trigger Q: Need families of related products (WinButton+WinCheckbox) and must keep them consistent?

One-liner: Interface of factory methods for each product in a family; concrete factories produce one family.

Hook: Cross-platform UI kit — GUIFactoryWinFactory / MacFactory.

type Button interface{ Paint() }
type Checkbox interface{ Paint() }

type GUIFactory interface {
    CreateButton() Button
    CreateCheckbox() Checkbox
}

type WinFactory struct{}
func (WinFactory) CreateButton() Button     { return WinButton{} }
func (WinFactory) CreateCheckbox() Checkbox { return WinCheckbox{} }

func Application(f GUIFactory) {
    f.CreateButton().Paint()
    f.CreateCheckbox().Paint()
}
Enter fullscreen mode Exit fullscreen mode

Trap: Adding a new product type forces changing the factory interface (and all factories).


Builder

Trigger Q: Constructor has 10+ params / many optional steps / same process, different representations?

One-liner: Build step-by-step; director can reuse the recipe; get product at the end.

Hook: House builder — walls, doors, roof; same steps → wooden or stone house.

type House struct{ Walls, Doors, Roof string }

type HouseBuilder interface {
    BuildWalls()
    BuildDoors()
    BuildRoof()
    GetHouse() House
}

type WoodBuilder struct{ h House }
func (b *WoodBuilder) BuildWalls()      { b.h.Walls = "wood" }
func (b *WoodBuilder) BuildDoors()      { b.h.Doors = "wood" }
func (b *WoodBuilder) BuildRoof()       { b.h.Roof = "wood" }
func (b *WoodBuilder) GetHouse() House  { return b.h }

type Director struct{}
func (Director) Construct(b HouseBuilder) House {
    b.BuildWalls(); b.BuildDoors(); b.BuildRoof()
    return b.GetHouse()
}
Enter fullscreen mode Exit fullscreen mode

Go tip: fluent setters (WithX() *Builder) are an idiomatic Builder variant.

Trap: Overkill for simple structs — use functional options for light cases.


Prototype

Trigger Q: Creating from scratch is expensive / I don't want to depend on concrete classes to copy?

One-liner: Clone existing objects via a common Clone() contract.

Hook: Cell mitosis; shape editor duplicate.

type Shape interface {
    Clone() Shape
    Draw()
}

type Circle struct {
    X, Y, R int
    Color   string
}

func (c Circle) Clone() Shape {
    cp := c // shallow copy; deep-copy slices/maps if needed
    return &cp
}
func (c Circle) Draw() { /* … */ }
Enter fullscreen mode Exit fullscreen mode

Trap: Deep vs shallow copy bugs with nested references.


Singleton

Trigger Q: Exactly one instance + global access (config, logger, connection pool)?

One-liner: Ensure one instance; provide a single access point.

Hook: Government / one president.

package config

import "sync"

type Config struct{ DSN string }

var (
    once sync.Once
    inst *Config
)

func Get() *Config {
    once.Do(func() { inst = &Config{DSN: "postgres://…"} })
    return inst
}
Enter fullscreen mode Exit fullscreen mode

Trap: Hidden global state; hard to test — prefer DI; if needed, use sync.Once (thread-safe).

Pros: controlled access, lazy init. Cons: violates SRP often; masks dependencies.


6. STRUCTURAL — "how are objects wired?"

mindmap
  root((Structural))
    Adapter
      make incompatible APIs work
    Bridge
      split abstraction from implementation
    Composite
      tree of part / whole
    Decorator
      wrap to add behavior
    Facade
      simplify a subsystem
    Flyweight
      share intrinsic state RAM
    Proxy
      stand-in controlling access
Enter fullscreen mode Exit fullscreen mode

Adapter

AKA: Wrapper

Trigger Q: Third-party / legacy API shape ≠ what my code expects?

One-liner: Translate one interface into another.

Hook: Power plug adapter.

type JSONAnalytics interface {
    AnalyzeJSON(data []byte) (string, error)
}

// legacy
type XMLService struct{}
func (XMLService) AnalyzeXML(xml string) string { return "ok" }

type XMLToJSONAdapter struct{ Inner XMLService }

func (a XMLToJSONAdapter) AnalyzeJSON(data []byte) (string, error) {
    xml := jsonToXML(data) // conversion
    return a.Inner.AnalyzeXML(xml), nil
}
Enter fullscreen mode Exit fullscreen mode

Confusion: Adapter changes interface; Decorator keeps interface & adds behavior; Facade simplifies a subsystem.


Bridge

Trigger Q: Two independent dimensions both need to vary (shape × renderer; remote × device)?

One-liner: Split into Abstraction + Implementation hierarchies linked by composition.

Hook: Remote control (abstraction) ↔ Device (TV/Radio implementation).

type Device interface {
    IsOn() bool
    On(); Off()
    SetVolume(int)
}

type Remote struct{ Dev Device }

func (r *Remote) Toggle() {
    if r.Dev.IsOn() {
        r.Dev.Off()
    } else {
        r.Dev.On()
    }
}

type AdvancedRemote struct{ Remote }
func (r *AdvancedRemote) Mute() { r.Dev.SetVolume(0) }
Enter fullscreen mode Exit fullscreen mode

Trap: Looks like Strategy; Bridge is about structural decoupling of two hierarchies long-term.


Composite

Trigger Q: Tree of objects; clients should treat leaf and group the same?

One-liner: Uniform component interface for leaves and composites.

Hook: File system / org chart / nested boxes in a graphics editor.

type Graphic interface {
    Draw()
    Move(dx, dy int)
}

type Dot struct{ X, Y int }
func (d *Dot) Draw()            { /* point */ }
func (d *Dot) Move(dx, dy int)  { d.X += dx; d.Y += dy }

type Compound struct{ Children []Graphic }
func (c *Compound) Draw() {
    for _, ch := range c.Children {
        ch.Draw()
    }
}
func (c *Compound) Move(dx, dy int) {
    for _, ch := range c.Children {
        ch.Move(dx, dy)
    }
}
Enter fullscreen mode Exit fullscreen mode

Decorator

AKA: Wrapper

Trigger Q: Add responsibilities at runtime without exploding subclasses?

One-liner: Stack wrappers that share the component interface.

Hook: Wearing clothes — layers.

type Notifier interface{ Send(msg string) }

type EmailNotifier struct{}
func (EmailNotifier) Send(msg string) { /* email */ }

type SMSDecorator struct{ Inner Notifier }
func (d SMSDecorator) Send(msg string) {
    d.Inner.Send(msg)
    /* also SMS */
}

type SlackDecorator struct{ Inner Notifier }
func (d SlackDecorator) Send(msg string) {
    d.Inner.Send(msg)
    /* also Slack */
}

// usage: SlackDecorator{SMSDecorator{EmailNotifier{}}}
Enter fullscreen mode Exit fullscreen mode

Confusion pair: Decorator = add behavior, same interface. Proxy = control access (lazy, auth, remote). Adapter = change interface.


Facade

Trigger Q: Client talks to a messy subsystem of many classes?

One-liner: One simple entry API over a complex subsystem.

Hook: Ordering pizza by phone — one number, kitchen chaos hidden.

type VideoConverter struct {
    // holds ffmpeg, codec, bitrate helpers…
}

func (VideoConverter) Convert(filename, format string) string {
    // orchestrate: decode → filter → encode → write
    return "output." + format
}
Enter fullscreen mode Exit fullscreen mode

Trap: Don't let Facade become a god object — keep it a thin orchestrator.


Flyweight

Trigger Q: Millions of similar objects; RAM blown by duplicated immutable data?

One-liner: Share intrinsic (immutable) state; pass extrinsic state in methods.

Hook: Forest of trees — shared TreeType (texture/color), many Tree positions.

type TreeType struct{ Name, Color, Texture string } // intrinsic, shared

type TreeTypeFactory struct{ cache map[string]*TreeType }

func (f *TreeTypeFactory) Get(name, color, texture string) *TreeType {
    key := name + color + texture
    if t, ok := f.cache[key]; ok {
        return t
    }
    t := &TreeType{name, color, texture}
    f.cache[key] = t
    return t
}

type Tree struct {
    X, Y int
    Type *TreeType // flyweight
}

func (t Tree) Draw() { /* use t.X,t.Y + shared Type */ }
Enter fullscreen mode Exit fullscreen mode

Proxy

Trigger Q: Need lazy load, access control, logging, caching, or remote stub in front of a real object?

One-liner: Same interface as the real subject; proxy delegates after extra work.

Hook: Credit card as proxy for bank account.

type Image interface{ Display() }

type RealImage struct{ Path string }
func (r *RealImage) Display() { /* heavy load from disk */ }

type ImageProxy struct {
    Path string
    real *RealImage
}

func (p *ImageProxy) Display() {
    if p.real == nil {
        p.real = &RealImage{Path: p.Path} // lazy
    }
    // optional: auth / cache / log
    p.real.Display()
}
Enter fullscreen mode Exit fullscreen mode

Types: virtual (lazy), protection (ACL), remote, logging/smart reference.


7. BEHAVIORAL — "how do objects talk / change?"

mindmap
  root((Behavioral))
    Chain of Responsibility
      pass request along handlers
    Command
      request as object
    Iterator
      traverse without exposing structure
    Mediator
      hub for colleague communication
    Memento
      snapshot / undo
    Observer
      pub-sub
    State
      behavior by internal state
    Strategy
      swap algorithms
    Template Method
      algorithm skeleton + hooks
    Visitor
      externalize operations on a structure
Enter fullscreen mode Exit fullscreen mode

Chain of Responsibility

AKA: CoR, Chain of Command

Trigger Q: Multiple handlers might process a request; avoid hard-coded if/else towers?

One-liner: Handlers linked in a chain; each handles or forwards.

Hook: Support tiers L1→L2→L3; corporate purchase approvals.

type Handler interface {
    SetNext(Handler) Handler
    Handle(amount float64) string
}

type base struct{ next Handler }
func (b *base) SetNext(h Handler) Handler { b.next = h; return h }
func (b *base) forward(amount float64) string {
    if b.next != nil {
        return b.next.Handle(amount)
    }
    return "unhandled"
}

type Manager struct{ base }
func (m *Manager) Handle(amount float64) string {
    if amount <= 1000 {
        return "manager approved"
    }
    return m.forward(amount)
}

type Director struct{ base }
func (d *Director) Handle(amount float64) string {
    if amount <= 10000 {
        return "director approved"
    }
    return d.forward(amount)
}
Enter fullscreen mode Exit fullscreen mode

Relates: often with Composite; can use Command as the request object.


Command

Trigger Q: Need undo, queue, log, or schedule operations?

One-liner: Encapsulate a request as an object (receiver + args).

Hook: Restaurant order ticket; remote control buttons.

type Command interface {
    Execute()
    Undo()
}

type Editor struct{ Text string }

type CopyCommand struct{ Ed *Editor; clipboard *string }
func (c *CopyCommand) Execute() { *c.clipboard = c.Ed.Text }
func (c *CopyCommand) Undo()    {}

type PasteCommand struct {
    Ed        *Editor
    clipboard *string
    backup    string
}
func (c *PasteCommand) Execute() {
    c.backup = c.Ed.Text
    c.Ed.Text += *c.clipboard
}
func (c *PasteCommand) Undo() { c.Ed.Text = c.backup }

type Button struct{ Cmd Command }
func (b Button) Click() { b.Cmd.Execute() }
Enter fullscreen mode Exit fullscreen mode

Iterator

Trigger Q: Walk a collection without exposing list/tree/graph guts?

One-liner: Iterator interface (HasNext/Next); collection creates it.

Hook: TV remote channel surfing; Go's iter / for range spirit.

type Iterator[T any] interface {
    HasNext() bool
    Next() T
}

type SliceIter[T any] struct {
    data []T
    i    int
}
func (it *SliceIter[T]) HasNext() bool { return it.i < len(it.data) }
func (it *SliceIter[T]) Next() T {
    v := it.data[it.i]
    it.i++
    return v
}
Enter fullscreen mode Exit fullscreen mode

Go note: prefer for range / iter.Seq in modern Go; pattern still useful for custom graphs.


Mediator

AKA: Intermediary, Controller

Trigger Q: Many objects tangled in pairwise references (UI dialog chaos)?

One-liner: Colleagues talk only to a mediator; mediator coordinates.

Hook: Airport control tower; chat room.

type Mediator interface {
    Notify(sender Colleague, event string)
}

type Colleague interface {
    SetMediator(Mediator)
}

type AuthDialog struct { /* form fields */ }

func (d *AuthDialog) Notify(sender Colleague, event string) {
    switch event {
    case "loginClick":
        // validate fields, call auth service…
    case "checkboxChanged":
        // enable/disable controls…
    }
}

type Button struct {
    med Mediator
}
func (b *Button) SetMediator(m Mediator) { b.med = m }
func (b *Button) Click()                 { b.med.Notify(b, "loginClick") }
Enter fullscreen mode Exit fullscreen mode

Confusion: Mediator centralizes communication; Observer broadcasts events (looser).


Memento

AKA: Snapshot

Trigger Q: Undo/rollback without violating encapsulation of originator state?

One-liner: Originator creates opaque memento; caretaker stores history.

Hook: Text editor undo stack; game save checkpoint.

type Memento struct{ state string } // opaque to outsiders in stricter designs

type Editor struct{ text string }
func (e *Editor) Type(s string)          { e.text += s }
func (e *Editor) Save() Memento          { return Memento{e.text} }
func (e *Editor) Restore(m Memento)      { e.text = m.state }

type History struct{ stack []Memento }
func (h *History) Push(m Memento) { h.stack = append(h.stack, m) }
func (h *History) Pop() (Memento, bool) {
    if len(h.stack) == 0 {
        return Memento{}, false
    }
    m := h.stack[len(h.stack)-1]
    h.stack = h.stack[:len(h.stack)-1]
    return m, true
}
Enter fullscreen mode Exit fullscreen mode

Observer

AKA: Event-Subscriber, Listener

Trigger Q: Many objects must react when one object's state changes?

One-liner: Subject maintains subscribers; notifies on change.

Hook: YouTube subscriptions; newspaper subscriptions.

type Observer interface{ Update(temp float64) }

type Subject interface {
    Attach(Observer)
    Detach(Observer)
    Notify()
}

type WeatherStation struct {
    temp float64
    obs  []Observer
}
func (w *WeatherStation) Attach(o Observer) { w.obs = append(w.obs, o) }
func (w *WeatherStation) SetTemp(t float64) {
    w.temp = t
    w.Notify()
}
func (w *WeatherStation) Notify() {
    for _, o := range w.obs {
        o.Update(w.temp)
    }
}

type PhoneDisplay struct{}
func (PhoneDisplay) Update(temp float64) { /* UI */ }
Enter fullscreen mode Exit fullscreen mode

Go tip: channels / event buses are common idioms; same pub-sub idea.


State

Trigger Q: Object behavior changes drastically with internal state (and you have state×method switch hell)?

One-liner: Delegate behavior to state objects; transitions swap the state.

Hook: Vending machine; document Draft→Moderation→Published.

type State interface {
    Publish(*Document)
}

type Document struct {
    state State
}
func (d *Document) SetState(s State) { d.state = s }
func (d *Document) Publish()         { d.state.Publish(d) }

type Draft struct{}
func (Draft) Publish(d *Document) { d.SetState(Moderation{}) }

type Moderation struct{}
func (Moderation) Publish(d *Document) { d.SetState(Published{}) }

type Published struct{}
func (Published) Publish(*Document) { /* already published */ }
Enter fullscreen mode Exit fullscreen mode

Confusion pair: State ≈ Strategy structurally; State is aware of transitions / tied to context lifecycle; Strategy is usually injected & unaware of siblings.


Strategy

Trigger Q: Family of interchangeable algorithms selected at runtime?

One-liner: Context holds a Strategy interface; swap implementations.

Hook: Navigation — driving / walking / public transport routes; payment methods.

type RouteStrategy interface {
    BuildRoute(a, b string) []string
}

type Navigator struct{ Strategy RouteStrategy }
func (n Navigator) Route(a, b string) []string { return n.Strategy.BuildRoute(a, b) }

type RoadStrategy struct{}
func (RoadStrategy) BuildRoute(a, b string) []string { return []string{a, "highway", b} }

type WalkStrategy struct{}
func (WalkStrategy) BuildRoute(a, b string) []string { return []string{a, "path", b} }
Enter fullscreen mode Exit fullscreen mode

Relates: OCP's poster child; similar to Bridge but focused on algorithms, not long-lived dual hierarchies.


Template Method

Trigger Q: Same algorithm skeleton; only some steps differ by subtype?

One-liner: Abstract steps in a fixed-order method; override hooks.

Hook: Data miner — open → extract → parse → analyze → close.

type DataMiner interface {
    Open()
    Extract() []byte
    Parse([]byte) any
    Analyze(any)
    Close()
}

func Mine(m DataMiner) { // template method as function (Go-friendly)
    m.Open()
    defer m.Close()
    raw := m.Extract()
    data := m.Parse(raw)
    m.Analyze(data)
}

type PDFMiner struct{}
func (PDFMiner) Open()              {}
func (PDFMiner) Extract() []byte    { return nil }
func (PDFMiner) Parse([]byte) any   { return nil }
func (PDFMiner) Analyze(any)        {}
func (PDFMiner) Close()             {}
Enter fullscreen mode Exit fullscreen mode

Relates: Factory Method is often one step inside a Template Method.

Trap: Heavy inheritance; in Go prefer function template + interface hooks (as above).


Visitor

Trigger Q: Many unrelated operations on an object structure, and you can't keep stuffing methods into element classes?

One-liner: External visitor with VisitConcreteA/B; elements Accept(v).

Hook: Insurance company agent visiting buildings; export/XML/JSON ops on a document AST.

type Shape interface {
    Accept(Visitor)
}

type Visitor interface {
    VisitDot(*Dot)
    VisitCircle(*Circle)
}

type Dot struct{ X, Y int }
func (d *Dot) Accept(v Visitor) { v.VisitDot(d) }

type Circle struct{ X, Y, R int }
func (c *Circle) Accept(v Visitor) { v.VisitCircle(c) }

type XMLExportVisitor struct{}
func (XMLExportVisitor) VisitDot(d *Dot)       { /* export dot */ }
func (XMLExportVisitor) VisitCircle(c *Circle) { /* export circle */ }
Enter fullscreen mode Exit fullscreen mode

Trap: Adding new element types is painful (must update Visitor). Best when element set is stable and operations grow.


8. Decision tree (interview speed)

flowchart TD
  Start{What do you need?}
  Start --> Create[CREATE]
  Start --> Structure[STRUCTURE / wrap]
  Start --> Behave[BEHAVIOR / communication]

  Create --> C1[one of several products] --> FM[Factory Method]
  Create --> C2[family of products] --> AF[Abstract Factory]
  Create --> C3[complex step build] --> B[Builder]
  Create --> C4[copy existing] --> P[Prototype]
  Create --> C5[exactly one] --> S[Singleton — last resort]

  Structure --> S1[incompatible API] --> Ad[Adapter]
  Structure --> S2[two varying hierarchies] --> Br[Bridge]
  Structure --> S3[tree part/whole] --> Co[Composite]
  Structure --> S4[add behavior same API] --> De[Decorator]
  Structure --> S5[simplify subsystem] --> Fa[Facade]
  Structure --> S6[share RAM state] --> Fl[Flyweight]
  Structure --> S7[control access / lazy / remote] --> Pr[Proxy]

  Behave --> B1[pass along handlers] --> Ch[Chain of Responsibility]
  Behave --> B2[undo / queue / log action] --> Cm[Command]
  Behave --> B3[traverse collection] --> It[Iterator]
  Behave --> B4[untangle many-to-many talk] --> Me[Mediator]
  Behave --> B5[snapshot / undo state] --> Mm[Memento]
  Behave --> B6[notify many listeners] --> Ob[Observer]
  Behave --> B7[behavior by state machine] --> St[State]
  Behave --> B8[swap algorithm] --> Sy[Strategy]
  Behave --> B9[shared algorithm skeleton] --> Tm[Template Method]
  Behave --> B10[add ops to stable structure] --> Vi[Visitor]
Enter fullscreen mode Exit fullscreen mode

9. Confusion pairs (drill these)

Pair Difference
Adapter vs Decorator vs Proxy change interface / add behavior / control access
Adapter vs Facade one object vs whole subsystem simplification
Strategy vs State injected algorithm vs self-transitioning states
Strategy vs Bridge algorithm swap vs dual hierarchy decoupling
Mediator vs Observer central coordinator vs distributed pub-sub
Chain vs Decorator stop/forward request vs stack all behaviors
Factory Method vs Abstract Factory one product hook vs product families
Composite vs Decorator tree of children vs linear wrapper chain
Command vs Strategy request object (often undoable) vs replaceable algorithm
Template Method vs Strategy inheritance/hooks vs composition/delegate

10. LLD practices checklist (from the book → Go habits)

  1. Find what varies → extract interface / strategy / state.
  2. Depend on interfaces → accept interface{…} at boundaries.
  3. Compose → embed small collaborators; avoid deep type hierarchies.
  4. SRP → split God structs.
  5. OCP → add types, don't edit battle-tested cores.
  6. LSP → implementations must honor the contract.
  7. ISP → small interfaces (io.Reader style).
  8. DIP → high-level packages define interfaces; infra implements.
  9. Name the pattern in design docs — shared language.
  10. Don't pattern-hunt — complexity only when change pressure justifies it.

11. Quick Go idiom map (book OOP → Go)

Book concept Go idiom
Abstract class Interface + optional helper funcs
Protected members Same package / unexported
Multiple inheritance Interface embedding
Polymorphism Implicit interface satisfaction
Singleton sync.Once or DI container
Observer interfaces, channels, event bus
Iterator for range, iter.Seq
Decorator/Proxy wrapping structs implementing same interface

Top comments (0)