Irbisa · Cheatsheet

Architecture & Patterns in Go

SOLID in Go, Clean and hexagonal architecture, DDD building blocks, CQRS, Go-idiomatic patterns, dependency injection

Middle Developer · 17 questions

  1. 01

    What do the five SOLID principles say, and what does each one look like in Go, a language without classes or inheritance?

    SOLID is a set of five design rules: single responsibility, open/closed, Liskov substitution, interface segregation and dependency inversion.

  2. 02

    What do DRY, KISS and YAGNI mean, and when does removing duplication make Go code worse?

    DRY (don't repeat yourself) says each piece of knowledge lives in one place, KISS (keep it simple) says choose the simplest design that works, and YAGNI (you aren't gonna need it) says do not build for needs you only imagine.

  3. 03

    What do low coupling and high cohesion mean, and how do a Go package's imports and exported names show that it has neither?

    Coupling is how much one package depends on the details of others; cohesion is how closely the code inside one package belongs together.

  4. 04

    ReadOnlyCache satisfies Cache by returning an error from Set. Which SOLID principle does it break, and how do you fix the design?

    type Cache interface {	Get(ctx context.Context, key string) ([]byte, bool)	Set(ctx context.Context, key string, val []byte) error} var ErrReadOnly = errors.New("cache is read-only") // Serves a snapshot embedded in the binary.type ReadOnlyCache struct{ data map[string][]byte } func (c *ReadOnlyCache) Get(_ context.Context, key string) ([]byte, bool) {	v, ok := c.data[key]	return v, ok} func (c *ReadOnlyCache) Set(context.Context, string, []byte) error { return ErrReadOnly }

    It breaks the Liskov substitution principle: code written against Cache expects Set to store the value, and ReadOnlyCache compiles but breaks that promise at run time.

  5. 05

    What is the difference between Dependency Inversion, Dependency Injection and Inversion of Control, and which of them does a hand-wired Go main use?

    Dependency inversion is a design rule: business code depends on an interface it owns.

  6. 06

    How do you do dependency injection in Go, and do you need a framework like wire or fx?

    Pass dependencies in explicitly, as constructor parameters stored in struct fields, and wire the whole graph by hand in main.

    package main import (	"context"	"database/sql"	"log"…
  7. 07

    Which classic design patterns do you actually use in Go, and which ones does the language make unnecessary or reduce to a single function?

    Most patterns survive but shrink to a language feature: Strategy is an interface or a function value, Decorator is middleware, Builder becomes functional options, Singleton is sync.OnceValue, Iterator is iter.Seq.

  8. 08

    How do you implement the Strategy pattern in Go, and when is a function type like http.HandlerFunc better than a one-method interface?

    A strategy is a value the caller passes in to pick the behaviour: a one-method interface or simply a function.

    type Order struct {	TotalCents int64	Coupon     string} // The strategy: how the discount is computed.…
  9. 09

    Find the bug: after wrapping UserRepo in a caching decorator, UpdateEmail succeeds but Get keeps returning the old email until the TTL expires.

    type cachedRepo struct {	UserRepo // methods not defined below go straight to the database	cache    *ttlCache[int64, User]} func (r *cachedRepo) Get(ctx context.Context, id int64) (User, error) {	if u, ok := r.cache.Get(id); ok {		return u, nil	}	u, err := r.UserRepo.Get(ctx, id)	if err == nil {		r.cache.Set(id, u)	}	return u, err}

    The decorator embeds UserRepo, so UpdateEmail is promoted from the inner repository: writes go straight to the database and never touch the cache.

  10. 10

    What are Clean Architecture and hexagonal (ports and adapters) architecture, what is the dependency rule, and when is it overkill for a Go service?

    Both put the business rules in the centre and everything technical (HTTP, SQL, Kafka, third-party APIs) at the edge, under one rule: source code dependencies point only inward.

  11. 11

    What makes a repository interface leak the database (*sql.Rows, SQL fragments, ORM models in its signatures), and what should it expose instead?

    It leaks when its signatures carry storage types or storage concepts, because then every caller depends on the database through the interface that was meant to hide it.

    // wrong: the database leaks through the interface.type LeakyOrderRepo interface {	Query(ctx context.Context, where string, args ...any) (*sql.Rows, error)	Get(ctx context.Context, id int64) (OrderRow, error) // sql.NullString fields inside}…
  12. 12

    How do you keep a third-party SDK's types (a payment provider, an S3 client) out of your domain code, and what does an anti-corruption layer buy you?

    Declare a port in your own terms (payment.Gateway with your Charge type and ErrDeclined) and implement it in one adapter package, the only one that imports the SDK and translates its types, statuses and errors.

    // package payment: the port, in the domain's own terms.var ErrDeclined = errors.New("payment declined") type Charge struct {	AmountCents    int64	Currency       string // ISO 4217: "EUR"…
  13. 13

    What are an entity, a value object and an aggregate in DDD, and how do you protect an aggregate's invariants in Go when struct fields are exported?

    An entity has an identity that survives changes (an order with its id), a value object is defined only by its values and never changes (Money), and an aggregate is a cluster of them changed only through its root, which guards the invariants.

    // Money is a value object: no identity, compared with ==, never mutated.type Money struct {	Cents    int64	Currency string}…
  14. 14

    Go has no sealed classes or enums with data. How do you model a closed set of variants, such as payment methods or order states, so a missing case is caught?

    Use an interface with an unexported marker method: only its own package can declare that method, so the set is closed.

    // Method is a closed set: isMethod is unexported, so no other package can add a variant.////sumtype:decltype Method interface{ isMethod() } type Card struct{ Last4 string }…
  15. 15

    What is a bounded context in DDD, and how does it decide where one Go package or service ends and the next one begins?

    A bounded context is the boundary inside which one model and its terms have one meaning: a Product in the catalogue (description, photos) is not the Product in the warehouse (weight, shelf) or in billing (price, tax).

  16. 16

    What is CQRS, when is a separate read model worth it, and what does it cost you in consistency?

    CQRS (command query responsibility segregation) splits the code that changes state from the code that reads it: commands go through the domain model, queries read a model shaped for the screen.

  17. 17

    What is event sourcing, how does it differ from storing current state plus an outbox, and what makes it hard to run in production?

    In event sourcing the append-only log of domain events is the source of truth, and current state is computed by replaying it.

    type Event interface{ isEvent() } type Placed struct{ ID string }type LineAdded struct{ Cents int64 }type Paid struct{}…