Value Objects in Go: Making Invalid Domain State Unrepresentable
#Dev

Value Objects in Go: Making Invalid Domain State Unrepresentable

Backend Reporter
2 min read

A struct with a plain string field lets any code create an invalid email, and the type system offers no protection. The HTTP handler checks format before persisting, but other callers may bypass that check, leading to invalid data in the database.

Featured image A struct with a plain string field lets any code create an invalid email, and the type system offers no protection. The HTTP handler checks format before persisting, but other callers such as a CSV importer, an admin tool, a webhook, or a migration script may bypass that check or apply a different rule.

The zero‑value trap Go provides a default value for every type, including a struct with no fields set. For a domain type this default is a trap because it can be persisted without any business rule being satisfied.

The field becomes lowercase, so code outside the package cannot write to it directly. The constructor normalizes the string once, trimming spaces and lowercasing the address, then checks the format with a regular expression. If validation fails an error is returned and no value is created.

Now we have a type that can only be obtained through NewEmail, and any instance is guaranteed to be non‑empty and syntactically correct. The handler, the importer, the admin tool, and the migration script all call NewEmail, so the invariant is enforced at the boundary between external data and the domain.

The repository can add an IsZero method that rejects a zero value before persisting, ensuring that no invalid email reaches the database. The check happens after the value object has been created, so the domain never sees an invalid state.

Consider money as another domain concept where scattered checks cause outages. Storing a monetary amount as a float64 invites rounding errors and currency mismatches.

A money value object stores the amount in minor units as an integer and keeps the currency attached. The constructor enforces that the currency is a three‑letter ISO code and that the amount is non‑negative for a price. Adding two Money objects requires the same currency; otherwise an error is returned, preventing accidental mixing of USD and EUR.

The cost of this approach is a small extra type and a constructor per concept. The payoff is that the “someone forgot to validate” bug cannot arise, because the type itself guarantees validity.

In a hexagonal architecture the value object resides in the domain layer, alongside entities and aggregates. Adapters at the edge translate external representations such as database rows or JSON payloads into the domain type by invoking the constructor.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture The longer version of this article appears in Hexagonal Architecture in Go, which expands on value objects, entities, and aggregates wired through ports and adapters. The Complete Guide to Go Programming covers the language fundamentals that make the pattern possible, including error handling and the zero‑value behavior.

Google article image

Comments

Loading comments...