rss twitter gitlab github linkedin linkedin instagram
Microservices in Go: Validations
Oct 29, 2021

Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.

Continuing with our Microservice example, this time I will share with you the process I follow for implementing validations.



Where to add validations?

The code used for this post is available on Github.

The project is structured following Domain Driven Design and Hexagonal Architecture, because of that I like to think about validations by determining where the inputs are coming from and what we should we doing about them, basically the interaction between each one of our layers, or to be much more clearer how the communication flows from each package, I call those Implicit Validations and Explicit Validations.

  • Implicit Validation, in the context of is the received input, is matching the expected values, and
  • Explicit Validation, in the context of is the received input, is what out business logic defines.

To elaborate, an Implicit Validation would apply in those cases were a received argument must match the types used in its fields, the most common example would be a JSON payload received via an HTTP request that is decoded into a type using json.Unmarshal, where the fields should satisfy the concrete schema; another example would be when using enums (via iota) where we validate the fields are within the boundaries of what we defined as valid.

For example, the Priority type:

25// Validate ...
26func (p Priority) Validate() error {
27	switch p {
28	case PriorityNone, PriorityLow, PriorityMedium, PriorityHigh:
29		return nil
30	}
31
32	return NewErrorf(ErrorCodeInvalidArgument, "unknown value")
33}

An Explicit Validation would be when Business Logic is involved, for example in our Microservice we have a Dates type, this type defines Business Logic that expect the Start Date to be before the End Date when the Start Date is present:

44// Validate ...
45func (d Dates) Validate() error {
46	if !d.Start.IsZero() && !d.Due.IsZero() && d.Start.After(d.Due) {
47		return NewErrorf(ErrorCodeInvalidArgument, "start dates should be before end date")
48	}
49
50	return nil
51}

Explicit Validations in Domain Types

Explicit Validations are typically added to the Domain Types, those types define all the Business Logic we should care about the concrete Entity, for example Task, defines a Validate method:

64// Validate ...
65func (t Task) Validate() error {
66	if t.Description == "" {
67		return NewErrorf(ErrorCodeInvalidArgument, "description is required")
68	}
69
70	if err := t.Priority.Validate(); err != nil {
71		return WrapErrorf(err, ErrorCodeInvalidArgument, "priority is invalid")
72	}
73
74	if err := t.Dates.Validate(); err != nil {
75		return WrapErrorf(err, ErrorCodeInvalidArgument, "dates are invalid")
76	}
77
78	return nil
79}

This Validate method contains all the required validations for this type to be valid, using this method with concrete logic to validate the fields is typical when implementing validations, other alternatives include using struct tags to define the validation rules instead of coding them directly, for example using go-playground/validator, however there’s still some sort of validate function to call to trigger it.

Implicit Validations in Data Stores

Implicit Validations are usually triggered during the data conversion between the received input and the final output meant to be used internally by types the package, one example of this is when using a Data Store, specifically a relational database like PostgreSQL that defines an ENUM column with concrete values:

36func newPriority(p internal.Priority) db.Priority {
37	switch p {
38	case internal.PriorityNone:
39		return db.PriorityNone
40	case internal.PriorityLow:
41		return db.PriorityLow
42	case internal.PriorityMedium:
43		return db.PriorityMedium
44	case internal.PriorityHigh:
45		return db.PriorityHigh
46	}
47
48	// XXX: because we are using an enum type, postgres will fail with the following value.
49
50	return "invalid"
51}

That unexported function is called during the Create call (and Update call) in our repository:

29// Create inserts a new task record.
30func (t *Task) Create(ctx context.Context, params internal.CreateParams) (internal.Task, error) {
31	id, err := t.q.InsertTask(ctx, db.InsertTaskParams{
32		Description: params.Description,
33		Priority:    newPriority(params.Priority),
34		StartDate:   newNullTime(params.Dates.Start),
35		DueDate:     newNullTime(params.Dates.Due),
36	})
37	if err != nil {
38		return internal.Task{}, internal.WrapErrorf(err, internal.ErrorCodeUnknown, "insert task")
39	}
40
41	return internal.Task{
42		ID:          id.String(),
43		Description: params.Description,
44		Priority:    params.Priority,
45		Dates:       params.Dates,
46	}, nil
47}

The important thing about this validation is that it happens during the database call and we don’t need to explicitly call it beforehand.

Validations using “DTO” types

DTO means Data Transfer Object, basically a type meant to indicate values used for communication between processes, in our case between packages or layers. In Go, there are no Objects but the idea is the same: a DTT, a Data Transfer Type, could be used to define concrete business logic that only applies to certain steps of our application.

For example we may have specific rules that only apply when a Task is created, that doesn’t apply when a Task is updated; in those cases I like to implement a concrete internal <Action><TypeName> type in the domain package (in this case internal) to define the rules associated with that action, something like CreateTask for example that represents the rules associated with the creation of a Task.

More specifically let’s look at this snippet:

 3import (
 4	validation "github.com/go-ozzo/ozzo-validation/v4"
 5)
 6
 7// CreateParams defines the arguments used for creating Task records.
 8type CreateParams struct {
 9	Description string
10	Priority    Priority
11	Dates       Dates
12}
13
14// Validate indicates whether the fields are valid or not.
15func (c CreateParams) Validate() error {
16	if c.Priority == PriorityNone {
17		return validation.Errors{
18			"priority": NewErrorf(ErrorCodeInvalidArgument, "must be set"),
19		}
20	}
21
22	t := Task{
23		Description: c.Description,
24		Priority:    c.Priority,
25		Dates:       c.Dates,
26	}
27
28	if err := validation.Validate(&t); err != nil {
29		return WrapErrorf(err, ErrorCodeInvalidArgument, "validation.Validate")
30	}
31
32	return nil
33}

You probably noticed I’m not following the convention I just mentioned above! This is because so far the only creatable type is Task so it’s a bit redundant to append the type name, however if I had more types I would name them accordingly.

Conclusion

Having this separation between Implicit and Explicit validations has helped me to consolidate must of the required business rules in one place, in the domain package, things like data type conversion and input validation are handled in other packages before reaching the domain package to make things clearer when determining what are the exact rules the business should follow and how we are implementing those rules or persist those records.

If you’re looking to sink your teeth into more Microservice-related topics I recommend the following links:


Back to posts