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.
These are based on my personal developer learning experience in building the backend services of the Dine In system following a microservice architecture.
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.
Why study interface design?
Some interfaces are easy to understand. Some interfaces provide an extensive list of options and empower users immensely, but suffer at the user experience of learning and using the interface. How might we find the balance between power and usability?
We study interface design because we want to balance empowering the users and achieving great user experience (usability, learning curve, etc).
What are interfaces?
Before we begin talking about interfaces and software architecture design patterns, we need (1) to detail the different kinds of scopes that are supported in hexagonal architecture and (2) to elaborate on what interfaces and data structures mean.
To help describe these concepts, I’ve prepared a Stackblitz repository that you can fork and play around with. We’ll embed parts of its code within this blog post for demonstration purposes.
First we discuss the concept of scopes.
Following the hexagonal architecture/ports & adapter architecture, the different kinds of scopes are the following:
- Component/service scope – This is the level where the interface for a certain component is defined. This is the port definition from which adapters (infrastructure code) must conform to.
- Infrastructure scope – This is the level where external services (or 3rd party services) implementations exist and conform to the interfaces defined at the component.
- REST handler scope – This is the consumer of the component. It utilizes the exposed functions by the component/service.
The stackblitz code snippets below have a folder structure that emulates these distinctions. Feel free to click on the left sidebar to open up the project explorer so that you can see the folder structure. There should be the
internal folder which is separated into three directories:
Interface and data structure design
Second, we discuss interfaces and data structures.
Interfaces (also sometimes referred to as contracts) are the set of functions or methods that is defined to be supported by a system.
There are three elements in the design of an interface.
- the name of the functions available on that interface, often named based on what it is supposed to do,
- the parameters needed by that function, and
- the return type of that function.
We can see an erroneous* example interface below, for an account component that is capable of registering new users:
Note that this file is found under
internal/component/account-mistake-1 as we will be demonstrating different mistakes that we might encounter along the way when it comes to interface design (* It’s not particularly erroneous in that it doesn’t work; but it is erroneous in the sense that it is brittle code).
Having discussed these two concepts (scopes and interfaces), we now follow a human-centric approach by identifying the pain points in our developer experience.
Why are bad interfaces painful?
Bad interface design is painful because any major changes to the top-level interface (component-scope interface definition) will break the existing third party implementations (read: infrastructure code). The more third party implementations supported for this component, the more code that needs to be re-visited to make sure it now conforms to the new interface.
Initiating this plumbing process of cross-checking all implementing infrastructure code to see if it can still conform to the interface is intimidating and scary for a developer- because it is likely that some infrastructure code will no longer be able to implement the API. This means change that may potentially no longer work. Without testing (unit tests and integration tests), this requires a lot of courage and boldness to initiate this change.
<INSERT LINK TO UNIT TESTS AND INTEGRATION TESTS>
Although frightening for a few, this challenge is a cost that we have to incur when we want to grow our software. Through learning we absorb more context and better design our interfaces later on. We identify dependency chains better, and define our interfaces better.
Why do these kinds of mistakes occur?
These kinds of problems occur for various reasons:
Reason 1: Sometimes, we design the component interfaces based on the first infrastructure we intend to implement.
This is an incorrect design approach because component interfaces are intended to be high-level, conceptual, interfaces designed for reusability and portability. It is not intended to be short-sighted and should be designed with future-proofing in mind.
Without thinking about future-proofing, the design of the interface will immediately be influenced by the nearby structures available: i.e. the infrastructure code. This applies a bias to the interface design at the component level, which tightly couples the component design based on the first implementation available.
Reason 2: Sometimes, we only have one pathway or algorithm flow in mind
This is somewhat related to the above reason. I personally experienced this during building of the payment service in the DineIn platform.
In the early stages, we built a way for the application to be able to charge credit cards through a three-step process: tokenize, authorize, capture. However, not all payment gateways support this flow. For example, Paygcc’s Benefit API skips through the tokenization and authorization process, and immediately returns a redirect URL to a page where the user must enter their credit card details securely on the benefit webapp.
Reflecting on this, I learn about one of my implicit assumptions: I personally thought that the three-step process (tokenize, authorize, capture) was the de-facto single pathway for payments processing. Encountering this third party service (Paygcc Benefit API), I learned that there was a separate flow needed for it. I completely did not see this coming simply because of a lack of experience in implementing different kinds of payment-related APIs.
This reinforces the idea that interface designers must be highly experienced and full of context. This is how they will be able to rapidly iterate through potential scenarios, drawing from their personal experiences. This is why it is difficult for a junior developer to design interfaces- they still lack the broadness of experience to conjure up counter examples to poke holes and test the resilience/brittleness of their interface design.