Interface design represents the foundation of robust Go applications. I’ve implemented seven advanced patterns that transform how you structure code, manage dependencies, and maintain clean architecture.
Interface Segregation for Focused Contracts
Small interfaces create more maintainable code. I design interfaces with single responsibilities rather than monolithic contracts that burden implementers with unnecessary methods.
type UserReader interface {
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
}
type UserWriter interface {
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
type UserRepository interface {
UserReader
UserWriter
}
This approach allows components to depend only on the operations they need. A read-only service accepts UserReader, while full CRUD operations use UserRepository. Testing becomes simpler because mocks implement fewer methods.
Dependency Injection Through Interface Parameters
Accept interfaces, return structs. This fundamental principle enables flexible component composition and seamless testing. I structure services to receive their dependencies through constructor injection.
type NotificationService struct {
emailSender EmailSender
smsSender SMSSender
logger Logger
}
func NewNotificationService(email EmailSender, sms SMSSender, log Logger) *NotificationService {
return &NotificationService{
emailSender: email,
smsSender: sms,
logger: log,
}
}
func (n *NotificationService) SendWelcome(ctx context.Context, user *User) error {
if err := n.emailSender.Send(ctx, user.Email, "Welcome", "Welcome to our platform"); err != nil {
n.logger.Error("failed to send welcome email", "error", err)
return err
}
return nil
}
This pattern allows runtime configuration of behavior without changing core business logic. Test implementations replace production dependencies cleanly.
Behavioral Interface Composition
Compose interfaces to create specialized contracts that express exact requirements. I combine multiple behavioral interfaces to define precise capabilities without over-specifying implementation details.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
type FileProcessor struct {
source ReadWriteCloser
}
func (f *FileProcessor) Process(ctx context.Context) error {
defer f.source.Close()
buffer := make([]byte, 1024)
n, err := f.source.Read(buffer)
if err != nil {
return err
}
processed := transform(buffer[:n])
_, err = f.source.Write(processed)
return err
}
This composition allows FileProcessor to work with any type that supports reading, writing, and closing operations, whether files, network connections, or custom implementations.
Strategy Pattern Implementation
Implement strategy patterns using interfaces to swap algorithms dynamically. I create pluggable behavior systems that adapt to different requirements without conditional logic.
type PricingStrategy interface {
Calculate(ctx context.Context, items []Item) (Price, error)
}
type StandardPricing struct{}
func (s *StandardPricing) Calculate(ctx context.Context, items []Item) (Price, error) {
var total Price
for _, item := range items {
total.Amount += item.BasePrice
}
return total, nil
}
type MemberPricing struct {
discountPercent float64
}
func (m *MemberPricing) Calculate(ctx context.Context, items []Item) (Price, error) {
var total Price
for _, item := range items {
discounted := item.BasePrice * (1 - m.discountPercent/100)
total.Amount += discounted
}
return total, nil
}
type PricingService struct {
strategy PricingStrategy
}
func (p *PricingService) SetStrategy(strategy PricingStrategy) {
p.strategy = strategy
}
func (p *PricingService) CalculateTotal(ctx context.Context, items []Item) (Price, error) {
return p.strategy.Calculate(ctx, items)
}
Strategy patterns eliminate complex conditional statements and enable runtime behavior modification. New pricing strategies integrate without modifying existing code.
Adapter Pattern for External Integration
Adapt external libraries and services to your domain interfaces. I wrap third-party dependencies behind custom interfaces to isolate external concerns from business logic.
type Logger interface {
Info(msg string, fields ...Field)
Error(msg string, fields ...Field)
Debug(msg string, fields ...Field)
}
type ZapLogger struct {
logger *zap.Logger
}
func (z *ZapLogger) Info(msg string, fields ...Field) {
zapFields := make([]zap.Field, len(fields))
for i, f := range fields {
zapFields[i] = zap.String(f.Key, f.Value)
}
z.logger.Info(msg, zapFields...)
}
func (z *ZapLogger) Error(msg string, fields ...Field) {
zapFields := make([]zap.Field, len(fields))
for i, f := range fields {
zapFields[i] = zap.String(f.Key, f.Value)
}
z.logger.Error(msg, zapFields...)
}
type LogrusLogger struct {
logger *logrus.Logger
}
func (l *LogrusLogger) Info(msg string, fields ...Field) {
entry := l.logger.WithFields(logrus.Fields{})
for _, f := range fields {
entry = entry.WithField(f.Key, f.Value)
}
entry.Info(msg)
}
Adapter patterns protect applications from external API changes and enable switching between different implementations based on environment or requirements.
Factory Pattern with Interface Returns
Create objects through factory functions that return interfaces. This pattern hides implementation details while providing flexibility in object creation and configuration.
type DatabaseConnection interface {
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
Close() error
}
type ConnectionConfig struct {
Driver string
Host string
Port int
Database string
Username string
Password string
}
func NewDatabaseConnection(config ConnectionConfig) (DatabaseConnection, error) {
switch config.Driver {
case "postgres":
return newPostgresConnection(config)
case "mysql":
return newMySQLConnection(config)
case "sqlite":
return newSQLiteConnection(config)
default:
return nil, fmt.Errorf("unsupported driver: %s", config.Driver)
}
}
type PostgresConnection struct {
db *sql.DB
}
func newPostgresConnection(config ConnectionConfig) (*PostgresConnection, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
config.Host, config.Port, config.Username, config.Password, config.Database)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
return &PostgresConnection{db: db}, nil
}
func (p *PostgresConnection) Query(ctx context.Context, query string, args ...interface{}) (Rows, error) {
return p.db.QueryContext(ctx, query, args...)
}
Factory patterns centralize object creation logic and enable configuration-driven instantiation while maintaining interface contracts.
Observer Pattern for Event-Driven Architecture
Implement observer patterns using interfaces to create loosely coupled event systems. Components subscribe to events without direct dependencies on publishers.
type Event interface {
Type() string
Timestamp() time.Time
}
type EventHandler interface {
Handle(ctx context.Context, event Event) error
CanHandle(eventType string) bool
}
type EventBus interface {
Subscribe(handler EventHandler)
Publish(ctx context.Context, event Event) error
}
type InMemoryEventBus struct {
handlers []EventHandler
mu sync.RWMutex
}
func NewEventBus() *InMemoryEventBus {
return &InMemoryEventBus{
handlers: make([]EventHandler, 0),
}
}
func (e *InMemoryEventBus) Subscribe(handler EventHandler) {
e.mu.Lock()
defer e.mu.Unlock()
e.handlers = append(e.handlers, handler)
}
func (e *InMemoryEventBus) Publish(ctx context.Context, event Event) error {
e.mu.RLock()
handlers := make([]EventHandler, len(e.handlers))
copy(handlers, e.handlers)
e.mu.RUnlock()
var wg sync.WaitGroup
errors := make(chan error, len(handlers))
for _, handler := range handlers {
if handler.CanHandle(event.Type()) {
wg.Add(1)
go func(h EventHandler) {
defer wg.Done()
if err := h.Handle(ctx, event); err != nil {
errors <- err
}
}(handler)
}
}
wg.Wait()
close(errors)
for err := range errors {
if err != nil {
return err
}
}
return nil
}
type UserCreatedEvent struct {
UserID string
Email string
timestamp time.Time
}
func (u *UserCreatedEvent) Type() string {
return "user.created"
}
func (u *UserCreatedEvent) Timestamp() time.Time {
return u.timestamp
}
type WelcomeEmailHandler struct {
emailService EmailSender
}
func (w *WelcomeEmailHandler) CanHandle(eventType string) bool {
return eventType == "user.created"
}
func (w *WelcomeEmailHandler) Handle(ctx context.Context, event Event) error {
userEvent, ok := event.(*UserCreatedEvent)
if !ok {
return fmt.Errorf("invalid event type")
}
return w.emailService.Send(ctx, userEvent.Email, "Welcome", "Welcome to our platform!")
}
Observer patterns enable reactive architectures where components respond to events without tight coupling. New handlers integrate seamlessly without modifying existing publishers.
These seven patterns transform Go applications into maintainable, testable, and flexible systems. Interface segregation reduces complexity, dependency injection enables modularity, and behavioral composition creates reusable abstractions. Strategy patterns eliminate conditional logic, adapters isolate external dependencies, factories centralize creation, and observers enable event-driven architectures.
Each pattern addresses specific architectural challenges while maintaining Go’s simplicity and performance characteristics. Implementing these patterns requires careful consideration of interface design, avoiding over-abstraction while ensuring sufficient flexibility for future requirements.
The combination of these patterns creates applications that adapt to changing requirements, integrate external systems cleanly, and maintain clear separation of concerns throughout the codebase.