On Hexagonal architecture : Common mistakes (Part 2)

silhouette of person holding glass mason jar

This blog series is intended to identify problems encountered in backend microservice design following the hexagonal architecture, and how we might design it better from the ground up.

I only started practicing hexagonal architecture last August 2020, so I am still quite new to it. I write about it now to reflect on my experience to distill it into a guideline or a principle. To craft a lamp for myself and others.

Introduction

Prerequisite reading to this blog post would be Herberto Garcia’s Hexagonal Architecture blog post and how he put it all together, or threedot’s Combining DDD, CQRS, and Clean Architecture. This blog series condenses my understanding of those blog posts fused with my own experiences and noted challenges.

  • Why? (Part 1) – We discuss the purpose of interface design and argue for its value, and discuss the impacts of bad and good interface design.
  • Common mistakes (Part 2) – We talk about scenarios that pose problems, and discuss actual code examples. We compare bad code with good code (not necessarily the best; but definitely better).
  • Better architects (Part 3) – We talk about the journey of software developers in mastering the variety of tools available in software design.

Common mistakes

As the second entry in this blog series, this blog post jumps straight into issues and challenges encountered in the developer experience.

Challenges, pain points in developer experience

Ultimately, we want to build resilient code. By resilient, we mean code that supports code change easily, or has an easy way of adding new features to the existing codebase.

If a piece of code is not resilient, we call it brittle code. Having brittle code means that code reusability is very low (some old code regularly gets thrown away), or integrating new features or supporting new implementations on the existing codebase is cumbersome or requires a lot of refactoring. It is more challenging to maintain and grow.

Mistake 1: Brittle interfaces

In our current example, we explore an account service which is capable of registering a user.

As shown above, the register method requires both a username and a password to perform the registration process.

What if the account registration process suddenly has new requirements: What if email will now be required? If that happens, then the register function signature has to change.

This is one of the simplest examples of software brittleness. The contract (or the interface signature) itself changed because of new business rules/policies. This is normal in software development.

What we want to achieve however is becoming resilient when these changes do arrive. We can do this by revising our interface design as such:

Mistake 2: Incorrectly defining domain models in the wrong scope

This problem occurs when the code for the interface or data structure is defined in the wrong place. An example for this mistake is demonstrated below.

This problem can occur when you define data structure definitions at the component scope which is incorrect. This is incorrect because it creates a new dependency: the component definition now depends on the data definitions that vary per implementation or 3rd party service.

This is demonstrated in the snippet below, where the component now appears to require the definitions for the GoogleUser, the FacebookUser, and the TwitterUser interfaces simply because it was part of the definition of LoginRequest .

This can be fixed by making sure that the 3rd party services interface definitions are found on the infrastructure scope and not in the component scope.

As you can see below, the domain.ts definitions of the account service component no longer has a definition for the login objects. They are simply referred to and accessed via the metadata field.

By defining LoginRequest as a generic login request wrapper, we release the dependency from the account service component on the third party data structure definitions.

As part of the LoginRequest, we can extract the information that is common for all log in requests. In the above example, we assume that password is required for all of the login options.

Mistake 3: Non-visible methods

This mistakes may occur when you are defining the capabilities that a third party service provides, but find that this method is not exposed at the service/driver.

Although the method is defined at the infrastructure scope, this incorrect interface design does not allow the service consumer (the entrypoint) to be able to access the method definitions.

<INSERT LINK TO STACKBLITZ EXAMPLE FOR ABOVE>

Mistake 4: Not recognizing dependencies properly

I’m still formulating this thought, so I’ll leave this as blank first.

This blog post discussing challenges on hexagonal architecture will lead up to another blog post called How to Design Interfaces. That blog post will describe the frame-of-mind that one must enter when designing interfaces so that we can maximize software resilience starting from interface design. Think abstractly, drawing from the concrete!

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.