Clean-Go for Web Services
by Bardia Keyoumarsi
Here at EasyPost we utilize the standard constructs within the Go language to set up our services with Clean Architecture in mind. In this article we will walk you through a Go web service setup and exercise a dependency migration that showcases the advantages of Clean Architecture.
Prerequisites
There are plenty of articles on the world wide web about the Clean Architecture philosophy. We suggest that a brief review of them should help you better understand the concepts used in this walkthrough.
Here is one that I have utilized in the past:
Web Service Setup
We typically break our service setup into four distinct segments:
- Repository packages that provide data access interfaces
- The entity package that implements the data constructs shared between packages
- Service packages that provide application/business logic
- External interface packages (HTTP controllers or CLI commands)
For our example walkthrough, we are going to set up a service that performs some specific business logic based on data that it receives from its repo packages. The service directory would look like the following:
The api
and cmd
packages house the external interfaces into our service. The only logic they contain is the process of receiving the external request (HTTP request, CLI command invocation) and setting up the expectations to use the business logic interfaces defined in the service
packages.
As previously mentioned, the entity
package holds the entity contracts between the repo, service, api, and cmd packages. This allows the application to scale its external interfaces independently of the intra-service dependencies.
And last but not least, we have the repo
and services
packages that respectively hold the data access and business logic interfaces.
Now for our example scenario, let's set up a repo package that handles event creation. Our initial datastore is a MySQL cluster.
As for the service
package, let's set up a capture
service that takes some inputs from the api
package, performs some business logic related to capturing events, and finally calls the repo/event
package interface to store the event data in the data store.
Backend Datastore Migration
Now this is where we outline the effectiveness of the Clean Architecture setup and using interface
data objects for our Go service.
Let's assume that we have reached a boiling point with our MySQL datastore and after exhaustive research we have determined that we need to migrate our datastore to a Cassandra backend.
Our migration plan could look something like:
- Set up a Cassandra backend store
- Have the service dual-write to both Cassandra and MySQL backends
- Verify data consistency across both data stores
- Flip the switch and only use the Cassandra backend
Let's set up repo/event
for dual writing to complete step 2 of our migration plan.
Now we have our code setup so that our events are stored in both MySQL and Cassandra data stores. This change is fully isolated to the repo/event
package. Neither the service
nor the api
/cmd
packages are affected by this change.
After gaining confidence in our Cassandra integration, it is time to flip the switch fully over to Cassandra insertions only. This change is going to be as simple as removing the now deprecated dualWriterRepo
and mySQLRepo
implementations. The NewRepo()
function will now just return a pointer to cassandraRepo{}
.
Other Advantages
Beside clean migrations of integrations across the service components, there are other positive gains from this approach.
Take unit testing for example. With your service components isolated in dedicated packages and use of interface{}
methods, we can easily setup mock interfaces for the package dependencies. This way, the unit tests for the individual package are focused on itself and we have the ability to enforce various edge cases through the use of the mock dependencies.
How about introducing new API releases? Want to change your API input parameters? Gopher it! The above approach should allow you to make isolated changes within the api
package. The different versions of your API then transform the input parameters into the required entity
construct used between the service components internally.
Conclusion
There are many ways you can set up your service structure. Ultimately, the project alongside the short-term and long-term needs should inform the approach. At a glance, the Clean Architecture approach may seem to be a bit more work than other approaches but the extra work pays dividends over the long-term, especially when scaling the application is one of the criterias.