Keeping Valid State in Memory
A design rule where domain objects enforce their own invariants so every instance is guaranteed to be correct.
Keeping valid state in memory means that an object (struct) can never exist in an incorrect state. If you hold a reference to it, you know it's valid. You don't need defensive checks scattered across handlers, and you don't have to worry about calling a Validate() method.
You achieve this through two mechanisms working together. First, constructors that validate all inputs before creating an object. Second, unexported fields that prevent anyone from bypassing those rules. The only way to create or modify the object is through its public methods, and each method enforces the transition rules.
type Order struct {
id OrderID
items []Item
shipped bool
}
func NewOrder(id OrderID, items []Item) (*Order, error) {
if id == "" {
return nil, errors.New("order ID is required")
}
if len(items) == 0 {
return nil, errors.New("order must have at least one item")
}
return &Order{
id: id,
items: items,
shipped: false,
}, nil
}
func (o *Order) Ship() error {
if o.shipped {
return ErrAlreadyShipped
}
o.shipped = true
return nil
}
Outside the order package, there's no way to build an Order except through NewOrder. The constructor rejects empty IDs and empty item lists. Every state transition goes through a method that checks the current state before changing it. No caller can flip the shipped field directly or skip a rule by assigning to a field.
The practical benefit is huge. When you're sure that every object you work with is correct, you stop writing if checks everywhere. You stop being afraid to touch code because you aren't sure of the side effects. Developing new features is much slower without confidence that you're using the code correctly.
This rule pairs naturally with value objects and entities. Value objects are immutable and validated at creation. Entities guard their state transitions through methods. Aggregates extend this idea to a cluster of objects that must stay consistent together. The Factory Pattern helps when creation logic is complex enough to deserve its own place.
One thing to watch for: the zero value. Go initializes structs to their zero value, which may not be a valid domain state. You can expose an IsZero() bool method to detect an Order that wasn't built through the constructor.
func (o Order) IsZero() bool {
return o.id == ""
}
Use it at the boundaries where a zero value could sneak in. For example, a function that operates on an Order should guard against receiving an uninitialized one:
func CapturePayment(o Order) error {
if o.IsZero() {
return errors.New("order is not initialized")
}
// charge the customer, record the payment, etc.
return nil
}
Without the check, a zero Order would slip through and CapturePayment would run against an invalid order. IsZero turns that silent bug into an explicit error at the edge.
References
- Introduction to DDD Lite: When microservices in Go are not enough — Introduces 'always keep a valid state in the memory' as the second rule of DDD Lite. Shows how unexported fields and constructors guarantee the Hour domain object is always valid.
- Combining DDD, CQRS, and Clean Architecture in Go — References the 'always keep a valid state in memory' rule when building the Training domain entity. Shows how constructor validation and encapsulated fields ensure the Training is always valid.
- Safer Enums in Go — Applies the valid-state-in-memory idea to enums. Struct-based enums keep structures always in a valid state in memory, making business logic easier to work with.
- Database Transactions in Go with Layered Architecture — Uses encapsulation to keep the User aggregate model always valid in memory. The only way to change state is by calling exported methods, preventing invalid transitions.