Mastering Go Struct Design: DDD Principles, Patterns, and Real‑World Practices
This article explores how to model clean, high‑cohesion Go structs using Domain‑Driven Design, value objects, entities, services, and proven patterns like functional options, builders, specifications, and repositories, illustrated with e‑commerce and payment system examples.
Why Struct Design Matters
Good struct design keeps related data and behavior together, which improves maintainability, testability, extensibility, and readability. Poor design leads to bloated structs and scattered logic.
Core Design Principles
High Cohesion, Low Coupling – Group fields and methods that belong to the same concept; separate unrelated concerns.
Express Business Constraints with Types – Use custom value objects and enums so illegal states cannot be represented.
Prefer Composition Over Inheritance – Embed reusable components explicitly instead of mimicking inheritance.
Interface Segregation & Dependency Inversion – Depend on abstractions (interfaces) rather than concrete implementations.
Domain Modeling Walkthrough
Step 1 – Identify Core Concepts (e.g., Order, OrderItem, Product, Payment, Shipping).
Step 2 – Define Value Objects such as OrderID, Money, Quantity, and Address. Value objects are immutable and validated on creation:
type Money struct {
amount int64 // stored in cents
currency Currency
}
func NewMoney(amount int64, currency Currency) (*Money, error) {
if amount < 0 {
return nil, errors.New("amount cannot be negative")
}
return &Money{amount: amount, currency: currency}, nil
}Step 3 – Define Entities with unique identifiers and mutable state. Example Order struct contains items, status, timestamps and methods for state transitions ( Cancel, Pay, Ship, Complete).
Step 4 – Define Domain Services for cross‑entity logic such as discount calculation or shipping fee computation. Services are injected via interfaces.
Struct Design Patterns
Functional Options – Avoid constructor explosion by configuring objects with option functions.
type OrderServiceOption func(*orderServiceConfig)
func WithDB(db *sql.DB) OrderServiceOption { return func(c *orderServiceConfig) { c.db = db } }
func NewOrderService(opts ...OrderServiceOption) (*OrderService, error) {
cfg := &orderServiceConfig{timeout: 30 * time.Second, retryCount: 3}
for _, opt := range opts { opt(cfg) }
if cfg.db == nil { return nil, errors.New("db is required") }
return &OrderService{db: cfg.db, cache: cfg.cache, logger: cfg.logger, timeout: cfg.timeout}, nil
}Builder Pattern – Step‑by‑step construction of complex aggregates.
builder := NewOrderBuilder().WithUser(userID).WithItem(item1).WithItem(item2).WithShippingAddr(addr)
order, err := builder.Build()Specification Pattern – Encapsulate reusable business rules for querying and validation.
type Specification[T any] interface {
IsSatisfiedBy(T) bool
And(Specification[T]) Specification[T]
Or(Specification[T]) Specification[T]
Not() Specification[T]
}
spec := OrderSpecs{}.IsPending().And(OrderSpecs{}.TotalAmountGreaterThan(*MustNewMoney(10000, CurrencyCNY)))Repository Pattern – Abstract data access. Example interfaces for MySQL and in‑memory implementations.
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id OrderID) (*Order, error)
// … other query methods
}Anti‑Patterns and Pitfalls
Over‑design – creating massive interfaces or structs when a simple struct suffices (YAGNI).
Deep nesting – avoid “struct‑of‑struct” hell; flatten where possible.
Ignoring zero‑value semantics – validate configuration fields instead of relying on defaults.
Pointer abuse – use pointers only when you need to represent “unset” or mutable shared state.
Payment System Case Study
Domain model includes value objects ( PaymentID, Money), the Payment entity with a status workflow, a Refund entity, gateway interfaces, and a PaymentService that orchestrates repository persistence, gateway calls, and event publishing.
func (p *Payment) StartProcessing(txNo TransactionNo) error {
if p.status != PaymentStatusPending { return errors.New("payment not in pending status") }
p.status = PaymentStatusProcessing
p.transactionNo = &txNo
p.updatedAt = time.Now()
return nil
}Core Principles Recap
High Cohesion – Keep related data and behavior together.
Low Coupling – Depend on abstractions, not concrete implementations.
Type Safety – Use the type system to enforce business rules.
Rich Domain{Model} – Encapsulate behavior inside entities rather than anemic structs.
Composition First – Favor composition over inheritance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ray's Galactic Tech
Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
