16. Design & Architecture
Keep boundaries explicit; invert dependencies; use functional options and DI for clarity.
Question: What is the "functional options pattern" and why is it used?
Answer: The functional options pattern is a way to provide clean, extensible, and non-intrusive configuration for constructors. Instead of a constructor with many arguments, you provide a variadic number of "option functions" (...Option
) that modify the object being created.
Explanation: This pattern solves several problems:
It avoids constructors with a long list of parameters (
New(a, b, c, d, ...)
).New options can be added without breaking existing code.
It provides clear, self-documenting configuration at the call site (
New(WithTimeout(5*time.Second), WithLogger(log))
).It allows for sensible defaults, as options are only applied if provided.
type Server struct { addr string; log *slog.Logger }
type Option func(*Server)
func WithAddr(a string) Option {
return func(s *Server) { s.addr = a }
}
func WithLogger(l *slog.Logger) Option {
return func(s *Server) { s.log = l }
}
func NewServer(opts ...Option) *Server {
s := &Server{ addr: ":8080", log: slog.Default() }
for _, o := range opts { o(s) }
return s
}
Question: How would you structure a Go application to follow clean architecture principles?
Answer: In Go, clean architecture is typically implemented by separating the application into layers: domain
(entities and business rules), usecase
(application-specific logic), and adapters
(interfaces to the outside world like databases, HTTP handlers, etc.). The key rule is that dependencies only point inwards: adapters depend on use cases, and use cases depend on the domain.
Explanation: This is achieved through the dependency inversion principle. Use cases define interfaces for the repositories or services they need, and the adapter
layer provides the concrete implementations. This decouples the core business logic from external concerns like the database or web framework, making the application easier to test, maintain, and refactor.
Question: How can you do dependency injection in Go without a framework? What about
wire
?
Answer: Prefer explicit constructors that accept dependencies as interfaces. For larger apps, use compile-time DI with google/wire
to generate wiring code.
Explanation: This keeps dependencies visible and testable. Avoid service locators or global singletons.
Question: How do you model domain errors and map them to transport?
Answer: Define domain-level error types (e.g., ErrNotFound
, ErrConflict
) and translate them at the boundary (HTTP/gRPC) into appropriate status codes.
Explanation: This preserves domain semantics while keeping transport concerns out of core layers.