--- name: solid description: 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 | |-----------|-----------|-----------------| | **S**RP | One reason to change | "Does this type have ONE reason to change?" | | **O**CP | Open for extension, closed for modification | "Can I extend without modifying?" | | **L**SP | Subtypes are substitutable | "Can any implementation replace another safely?" | | **I**SP | Small, focused interfaces | "Are clients forced to depend on unused methods?" | | **D**IP | 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:** ```go // 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:** ```go // 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:** ```go // 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:** ```go // 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. ```go // 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. ```go // 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