Files
skills/solid/SKILL.md
Mathias d6a71e370e
Some checks failed
release / tag (push) Has been cancelled
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab
next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill
dirs + SKILLS_INDEX.md) and adds the installation surface:

- Taskfile.yml — install / update / list / release / check targets
- install.sh — bootstrap installer for hosts without Task. Idempotent
  symlink wirer; default checkout at ~/.local/share/skills/ on every
  host; SKILLS_REF env var pins a tag (default: main).
- .gitea/workflows/release.yml — auto-tag every push to main by
  Bump-Type footer (major/minor/patch, default patch). Skipped when
  commit contains [skip-release].
- README — usage, versioning, contribution flow, secret-hygiene rule.

Phase 1 wires Claude Code only (~/.claude/skills/<name> global +
<repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode,
antigravity, and gitea-resident agents (cobalt-dingo, agentsquad)
once their skill conventions are researched.

Public repo, markdown-only — no secrets, no client names. Verified
via pre-push grep before initial push.

[skip-release]
2026-05-24 14:59:54 +02:00

9.3 KiB

name, description
name description
solid Apply SOLID design principles in Go. Use during architecture decisions, design reviews, and when adding new abstractions.

SOLID Principles

Overview

SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.

In Go, several principles manifest differently than in classical OOP languages. Where Go idioms conflict with OOP-centric SOLID, Go wins. The tension is noted explicitly.

Quick Reference

Principle One-Liner Question to Ask
SRP One reason to change "Does this type have ONE reason to change?"
OCP Open for extension, closed for modification "Can I extend without modifying?"
LSP Subtypes are substitutable "Can any implementation replace another safely?"
ISP Small, focused interfaces "Are clients forced to depend on unused methods?"
DIP Depend on abstractions "Do I accept interfaces, not concrete types?"

S — Single Responsibility Principle

"A class should have one, and only one, reason to change."

In Go: a type or package should have one reason to change. A package that handles both domain logic and database queries has two reasons to change.

Detection:

  • Can you describe the type's responsibility without using "and"?
  • Would different stakeholders (product, ops, DBA) request changes to different parts?

Go example:

// Bad: multiple responsibilities
type UserHandler struct {}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ... }
func (h *UserHandler) SaveToDatabase(u User) error { ... }
func (h *UserHandler) SendWelcomeEmail(u User) error { ... }

// Good: single responsibility each
type UserHandler struct {
    store  UserStore
    mailer Mailer
}

type PostgresUserStore struct { db *sql.DB }
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }

type SMTPMailer struct { client *smtp.Client }
func (m *SMTPMailer) SendWelcome(ctx context.Context, email string) error { ... }

O — Open/Closed Principle

"Software entities should be open for extension but closed for modification."

In Go: add new behavior by implementing an interface or adding a new type, not by modifying existing code. The canonical Go pattern is a switch on a string type that needs a new case every time — replace it with an interface.

Go example:

// Bad: must modify to add new payment method
func ProcessPayment(method string, amount Money) error {
    switch method {
    case "stripe":
        return stripeCharge(amount)
    case "paypal":
        return paypalCharge(amount)
    // Must add cases here for every new method!
    }
    return errors.New("unknown payment method")
}

// Good: open for extension via new types
type PaymentProcessor interface {
    Charge(ctx context.Context, amount Money) error
}

type StripeProcessor struct { client *stripe.Client }
func (p *StripeProcessor) Charge(ctx context.Context, amount Money) error { ... }

type PayPalProcessor struct { client *paypal.Client }
func (p *PayPalProcessor) Charge(ctx context.Context, amount Money) error { ... }

// Add new payment method: implement PaymentProcessor. No existing code changes.

L — Liskov Substitution Principle

"Subtypes must be substitutable for their base types without altering program correctness."

In Go: interfaces are structural. Any type that implements the method set satisfies the interface. This means callers should be able to use any implementation interchangeably without knowing the difference.

The key question: does your implementation honor the contract (documented behavior, not just method signatures)?

Go example:

// UserStore contract: Save persists a user and returns ErrDuplicate if email exists
type UserStore interface {
    Save(ctx context.Context, u User) error
    GetByEmail(ctx context.Context, email string) (User, error)
}

// Good: both implementations honor the contract
type PostgresUserStore struct { ... }
func (s *PostgresUserStore) Save(ctx context.Context, u User) error { ... }

type InMemoryUserStore struct { users map[string]User }
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
    if _, exists := s.users[u.Email]; exists {
        return ErrDuplicate // Must return ErrDuplicate, not some other error
    }
    s.users[u.Email] = u
    return nil
}

// Bad: violates contract — callers of UserStore cannot substitute this
type BrokenStore struct { ... }
func (s *BrokenStore) Save(ctx context.Context, u User) error {
    panic("not implemented") // Violates contract
}

Go tension: Go has no inheritance, so "refused bequest" (subclass ignoring parent methods) doesn't apply. The LSP concern in Go is about interface implementations that partially satisfy the contract through panics or no-ops.

I — Interface Segregation Principle

"Clients should not be forced to depend on methods they do not use."

In Go: this is idiomatic. The io.Reader, io.Writer, and io.Closer pattern is the model — small, focused interfaces that can be composed when needed.

Go example:

// Bad: fat interface — callers that only read must depend on write methods too
type FileStore interface {
    Read(name string) ([]byte, error)
    Write(name string, data []byte) error
    Delete(name string) error
    List(dir string) ([]string, error)
    Stats(name string) (FileInfo, error)
}

// Good: segregated interfaces — callers depend only on what they need
type FileReader interface {
    Read(name string) ([]byte, error)
}

type FileWriter interface {
    Write(name string, data []byte) error
}

type FileDeleter interface {
    Delete(name string) error
}

// Compose when needed
type FileReadWriter interface {
    FileReader
    FileWriter
}

// Handler that only reads: depends on FileReader only
func NewReportHandler(store FileReader) *ReportHandler { ... }

Go idiom: Define interfaces at the point of use, not at the point of implementation. The implementation package should not define the interface — the package that consumes it should.

// In the consumer package (handler)
type UserStore interface {
    GetByID(ctx context.Context, id UserID) (User, error)
}

// The postgres package doesn't need to know about this interface
// It just implements the method, and Go's structural typing handles the rest

D — Dependency Inversion Principle

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

In Go: pass dependencies as interface parameters. Never instantiate concrete dependencies inside a function or type that contains business logic.

// Bad: high-level order service depends on low-level email implementation
type OrderService struct {
    emailClient *sendgrid.Client // Locked to SendGrid
    db          *sql.DB         // Locked to PostgreSQL
}

// Good: depends on abstractions
type Mailer interface {
    Send(ctx context.Context, to, subject, body string) error
}

type OrderRepository interface {
    Save(ctx context.Context, o Order) error
    GetByID(ctx context.Context, id OrderID) (Order, error)
}

type OrderService struct {
    repo   OrderRepository
    mailer Mailer
}

func NewOrderService(repo OrderRepository, mailer Mailer) *OrderService {
    return &OrderService{repo: repo, mailer: mailer}
}

The Dependency Rule: Source code dependencies point inward toward domain logic, never outward toward infrastructure.

HTTP handlers → Application services → Domain types
Database layer → Application services → Domain types

Domain types know nothing about HTTP, SQL, or external APIs.

Applying SOLID at Architecture Level

Principle Package/Module Application
SRP Each package has one clear purpose
OCP New features = new packages/types, not edits to existing
LSP All implementations of an interface are interchangeable
ISP Interfaces defined at point of use, as narrow as possible
DIP Domain packages import nothing from infrastructure packages

Go Tensions with OOP-centric SOLID

OOP SOLID Go reality
Inheritance hierarchies for OCP Use interfaces + new types instead
Abstract base classes for LSP No inheritance; use interface contracts
Explicit interface declarations Interfaces are implicit; define where consumed
"Program to an interface" as ritual Only extract interface when you have 2+ implementations or need testability

Don't over-abstract. A function that takes a *sql.DB directly is fine if there's only one implementation and it's never tested in isolation. Extract an interface when you need it.

Red Flags

Flag Likely Violation
Type that "handles X and Y and Z" SRP
Large switch on a type string OCP
Implementation that panics on some methods LSP
Interface with 10+ methods ISP
new(ConcreteType) inside business logic DIP
Package imports something from infrastructure/ DIP

References

  • references/solid-principles.md — canonical SOLID reference with TypeScript examples
  • references/go-adaptation.md — this workspace's Go-specific rewrite of each principle
  • Load clean-code skill for naming and structure
  • Load code-review skill for detecting violations during review