Cohesion is part of the low coupling, high cohesion principle that’s supposed to keep your code maintainable. While low coupling means few dependencies, high cohesion roughly translates to single responsibility. Highly cohesive code (a module or a function) is focused on a single purpose. Low cohesion means it does many unrelated things.

I’ve written about coupling in the previous article on anti-patterns. Here are some tips on increasing cohesion in Go applications using the recently released generics.

The Application Logic

There’s a place in your project where your application logic lives. We call it the application layer and split it into commands and queries, CQRS style. Perhaps you call it handlers, services, controllers, or use cases. Hopefully, it’s not mixed with implementation details, like HTTP handlers.

Whatever you call this code, it’s probably not only logic. Some common operations need to happen for each request your application handles. They’re often related to observability, like logging, metrics, and tracing. Other examples are retries, timeouts, or an authorization mechanism.

Handler

Code sections in a typical handler.
func (h SubscribeHandler) Handle(ctx context.Context, cmd Subscribe) (err error) {
	start := time.Now()
	h.logger.Println("Subscribing to newsletter", cmd)
	defer func() {
		end := time.Since(start)
		h.metricsClient.Inc("commands.subscribe.duration", int(end.Seconds()))

		if err == nil {
			h.metricsClient.Inc("commands.subscribe.success", 1)
			h.logger.Println("Subscribed to newsletter")
		} else {
			h.metricsClient.Inc("commands.subscribe.failure", 1)
			h.logger.Println("Failed subscribing to newsletter:", err)
		}
	}()
}

Keeping such operations close to the logic decreases the cohesion. It mixes different concepts.

Eventually, it gets hard to grasp the logic itself. Even if you move some code to helper functions, you’ll end up passing many arguments and handling errors. You risk that someone copy-pastes the entire header of the function and forgets to change one of the fields. Updating the supporting code requires changing the logic parts.

The User Context

An interesting scenario is when you need to handle requests differently depending on the caller.

A typical example is checking the user’s permissions or attributes. You need to pass some kind of user context to the handler — a token, a user ID, or a complete User object. The logic can’t proceed without it.

Authorization middleware

A middleware transforming an authorization token into a user context.
user, err := UserFromContext(ctx)
if err != nil {
	return err
}

if !user.Active {
	return errors.New("the user's account is not active")
}

But often, there are a few entry points to do the same action. For example, in a system subscribing users to a newsletter, we could have:

  • A public HTTP API that uses the user context coming from a session token.
  • An internal gRPC API that other services call. An admin CLI tool also uses it.
  • A message handler that reacts to an event that already happened (a user signed up who agreed to receive a newsletter).
  • A migration that runs on the service’s startup. It backfills subscriptions that weren’t there because of a bug.

Except for the first entry point, all others miss a proper user context the logic could use

Ports

Multiple entry points to the same command handler.

You must manually craft and pass a fake user context for entry points other than the HTTP API.

fakeUser := User{
	ID:     "1", // Missing ID in the context, let's assume it's the root user making changes 
	Active: true,
}

ctx = ContextWithUser(ctx, fakeUser)

err := handler.Handle(ctx, cmd)
if err != nil {
	return err
}

Even if your application exposes only a public HTTP API, testing this function isn’t a good experience. You need to prepare a fake user every time you call the handler or mock it somewhere in the dependencies.

func TestSubscribe(t *testing.T) {
	logger := log.New(os.Stdout, "", log.LstdFlags)
	metricsClient := nopMetricsClient{}

	handler := NewSubscribeHandler(logger, metricsClient)

	user := User{
		ID:     "1000",
		Active: true,
	}

	ctx := ContextWithUser(context.Background(), user)

	cmd := Subscribe{
		Email:        "user@example.com",
		NewsletterID: "product-news",
	}

	err := handler.Handle(ctx, cmd)
	if err != nil {
		t.Fatal(err)
	}
}

The Decorator Pattern

Decorators are one of the least controversial design patterns. After all, HTTP middlewares are widely used, and it’s the same idea. You can think of decorators as application-level middlewares.

Decorators let you separate things that don’t belong together, but you can still explicitly use them where needed.

You wrap the logic with other operations. The logic handler doesn’t know what decorates it.

Decorators wrapping the command handler.
type subscribeAuthorizationDecorator struct {
	base SubscribeHandler
}

func (d subscribeAuthorizationDecorator) Handle(ctx context.Context, cmd Subscribe) error {
	user, err := UserFromContext(ctx)
	if err != nil {
		return err
	}

	if !user.Active {
		return errors.New("the user's account is not active")
	}

	return d.base.Handle(ctx, cmd)
}

The logic part stays super short now.

func (h subscribeHandler) Handle(ctx context.Context, cmd Subscribe) error {
	// Subscribe the user to the newsletter
	return nil
}

You can pick which decorators to use depending on the use case. You can set them up in constructors.

Different decorator sets for different purposes.
func NewAuthorizedSubscribeHandler(logger Logger, metricsClient MetricsClient) SubscribeHandler {
	return subscribeLoggingDecorator{
		base: subscribeMetricsDecorator{
			base: subscribeAuthorizationDecorator{
				base: subscribeHandler{},
			},
			client: metricsClient,
		},
		logger: logger,
	}
}

func NewUnauthorizedSubscribeHandler(logger Logger, metricsClient MetricsClient) SubscribeHandler {
	return subscribeLoggingDecorator{
		base: subscribeMetricsDecorator{
			base:   subscribeHandler{},
			client: metricsClient,
		},
		logger: logger,
	}
}

func NewSubscribeHandler() SubscribeHandler {
	return subscribeHandler{}
}

You can write a unit test for the exact same code but without things not related directly to the logic. You don’t need to mock the logger or the metrics client. Tests become shorter and easier to follow.

func TestSubscribe(t *testing.T) {
	handler := NewSubscribeHandler()

	cmd := Subscribe{
		Email:        "user@example.com",
		NewsletterID: "product-news",
	}

	err := handler.Handle(context.Background(), cmd)
	if err != nil {
		t.Fatal(err)
	}
}

Generic decorators

Splitting code into decorators comes with some boilerplate. Usually, we like to deal with boilerplate using code generation. For example, you can easily generate API or storage models.

We used to generate our decorators as well. But with Go 1.18 out there, we can now replace them with generic decorators.

Because all our command handlers follow the same convention, the command is the only generic type we need. (A command is a struct containing all inputs for the command handler.)

type CommandHandler[C any] interface {
	Handle(ctx context.Context, cmd C) error
}
type authorizationDecorator[C any] struct {
	base CommandHandler[C]
}

func (d authorizationDecorator[C]) Handle(ctx context.Context, cmd C) error {
	user, err := UserFromContext(ctx)
	if err != nil {
		return err
	}

	if !user.Active {
		return errors.New("the user's account is not active")
	}

	return d.base.Handle(ctx, cmd)
}

You can use the generic decorators with any other command handler. You could even move them to a separate library.

func NewUnauthorizedSubscribeHandler(logger Logger, metricsClient MetricsClient) CommandHandler[Subscribe] {
	return loggingDecorator[Subscribe]{
		base: metricsDecorator[Subscribe]{
			base:   SubscribeHandler{},
			client: metricsClient,
		},
		logger: logger,
	}
}

One Caveat

In most cases in Go, it makes sense to accept interfaces and return structs. But to make wrapping possible, you need to return an interface from constructors.

type SubscribeHandler interface {
	Handle(ctx context.Context, cmd Subscribe) error
}

type subscribeHandler struct{}

func NewSubscribeHandler() SubscribeHandler {
	return subscribeHandler{}
}

The downside is that it’s more challenging to navigate the project. You can’t just click the Handle method to land in the definition. You should be fine if you use a solid IDE that shows what implements an interface.

Also, be aware that it can surprise someone trying to follow the general rule. It’s OK to break the rules but understand (and be ready to explain) why you make an exception.

Another thing to watch out for is how you wrap your decorators. Outer decorators get executed first, and sometimes the order makes a difference. For example, you want to keep logs and metrics even if the authorization fails.

The Complete Example

Check the complete source code on the anti-patterns repository.

It features logging and metrics decorators ready to use with minimal changes.

Wild Workouts

In parallel, Robert introduced decorators to Wild Workouts, the example project you might know from our previous posts. Check the commit to see what this looks like in a real application.

There are two helper functions not covered in this post: ApplyCommandDecorators and ApplyQueryDecorators.

Robert also added support for running Wild Workouts locally on M1 CPUs. If you didn’t have a chance to do it yet, it might be a good moment to play with the project.