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.

Ray's Galactic Tech
Ray's Galactic Tech
Ray's Galactic Tech
Mastering Go Struct Design: DDD Principles, Patterns, and Real‑World Practices

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

design-patternsbackend architectureGoDomain-Driven Designclean codeStruct Design
Ray's Galactic Tech
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.