DEV Community

M Yauri M Attamimi
M Yauri M Attamimi

Posted on • Updated on

Clean Code Architecture in Go

Everyone can write code to produce a software that functionally works as expected, yet not everyone care about how's their code architecture look like. The best chosen algorithms not always save us from the bad software design. There are many code architectures around, and recently i've been struggling and trying to promote the Clean Code Architecture from Robert C. Martin a.k.a. Uncle Bob. Let's start with how this sort of code architecture looks like in Go.

I’m gonna start by implementing some of the functionalities within a shopping cart since it’s pretty straightforward. Bear in mind that what i’ll be showing here is restricted to the bounded context of the shopping cart, means we’ll just focus with the process of how a user/buyer choose the item and adding that particular item into his cart, and subsequently continue with the checkout. As for the billing/payment part, that will be in the bounded context of another service such as payment service. (if you’re neither really familiar nor confidence with what bounded context is, i would suggest you to read another article of mine from here.

By the way, all of the source code i explain here can be cloned/forked from

GitHub logo yauritux / clean-code-architecture

My personal opinion on the Clean Code Architecture in the context of Domain Driven Design (DDD), yet i adopted some terms from the Onion Architecture such as domain to avoid any misleading interpretations with the Entity in DDD

Common Shopping Cart Process

What are highlighted in the grey colors were standing outside of our shopping cart bounded context. Hence, we won’t tackle the nitty gritty details of those objects within our code implementation since those things would be in other services (e.g. product catalogue service and user service). Nevertheless, we’ll still be having those objects within our code, in the context of what our shopping cart needs. For instance, we won’t need user credentials information in our shopping cart since that should be catered from the user service context. Basically, we just need user information which is related to the shopping cart itself, such as: user_session_id, shipping address, billing address (furthermore, address information should also be catered from another service, e.g. : address service, even this is also optional).

There are some rules for this architecture those we must bear within our mind. I've summarized those rules as following:

  1. Dependency between layer should be going inward in one direction. As far as i concern, it was designed like this to avoid any circular dependencies.
  2. A layer should depend only on one direct layer (one level) beneath it, it shouldn’t know any layers beyond that level. For instance, A depends on B ( A → B ), and B depends on C ( B → C ), yet A shouldn’t know anything about C.
  3. Each layer should depends upon abstraction ( interface ) rather than implementation. This complies with what Uncle Bob said about 2 rules to fix RFI issues, you can read it from my old article here.

Let’s jump to the implementation by creating our project folder from the terminal and also initialize the go module as shown below:

mkdir cart-service && cd cart-service
go mod init github.com/yauritux/cart-svc

Next step is creating our pkg directory (mkdir pkg) from the same terminal. This pkg directory will contain our implementation logic for the shopping cart which complies with the clean code architecture we’re discussing here.

By referring to the depicted shopping cart process diagram earlier, we’ll be having these following entities within our code:

  1. User (this also represents a Buyer in the shopping cart bounded context).
  2. Cart (as the name implies, this is our cart object which holds our shopping items).
  3. Product (used as a placeholder for the product information. And for the sake of simplicity…, we won’t have any product categories because our goal here was merely to discuss the Clean Code Architecture).

I create those 3 entities within my pkg/domain/entity folder. To keep it simple, i deliberately marked all of the fields as exported fields (moving forward, we can think about hiding the fields information behind the setter / getter which are commons in Java).

Cart Entity:

User Entity:

Product Entity:

And here our value objects ( pkg/domain/valueobject ):

  1. Cart Items (strongly related to Product entity).
  2. Buyer Address (comprises of Billing and Shipping addresses, determined by the Address Type).

CartItem Value Object:

BuyerAddress Value Object:

Additionally, we will also have our enums as shown below:

If you’re wondering about the differences between entities and value objects, you can read it from

.

The next important thing to do is to implement our core business logic, which is part of Entities in Clean Code Architecture, or Core Domain in terms of Onion Architecture. This is something that is related to the enterprise business rules out of the application service rule.

In order to implement this kinda thing, we need to setup our aggregate root (i call it as user_cart aggregate). Again, take a look at what i wrote here if you’re still wondering about what is the aggregate all about.

Another to-do thing is to setup our repositories. Bear in mind, that when we’re talking about repository, it’s not always about the database. The storage implementation could be anything such as: web services, file, in-memory, etc.., and they should be abstracted by following some contracts defined on the interface. In that way, we can use them interchangeably later since we make our client code depends on the interface rather than directly on the implementation. In order to make it possible, we should start by abstracting our repositories with interfaces such as following:

Cart Repository Interface:

User Repository Interface:

Product Repository Interface:

I would assume that some of you will be asking about those empty interfaces :-). Why do they exist ? Why do we use them ?.

As far as i concern, we’ll be having different models for every layer in order to make our architecture more loosely-coupled. In that case, we should abstract our interface model as far as we can, thus we can have more flexibility within our code. Let’s say for the user repository interface, we can have 2 implementations for it, one implementation to get the user information from the database, and another one to get the information from a web service (e.g. REST). Therefore, definitely we’ll have 2 models here, one based on the database model, and another one is based on the web request-response model. That’s the reason why we use empty interface ( interface{} ) within our repository interface. You get it right ? :-). Don’t worry, everything will become clear once we implement our interface.

For the sake of simplicity, we'll be implementing just in-memory repository for now since our first goal is merely to discuss about how’s our clean code architecture will be looked like. However, i will incrementally update the code later for another kind of storage / repository, such as database or web service.

Let’s start to implement our repository interface by creating another directory (at the same level with our domain directory) as shown below:

mkdir -p pkg/adapter/repository/inmem/model

And here are our models (inside the pkg/adapter/repository/inmem/model directory) to be used in the context of our in-memory repository (remember about the interface{} before, when we designed our repository contract interface).

User Model for In-Memory Repository Implementation:

Cart Model for In-Memory Repository Implementation:

Product Model for In-Memory Repository Implementation:

subsequently followed by our in-memory repository implementation for those 3 models respectively (inside /adapter/repository/inmem directory):

User In-Memory Repository Implementation:

Cart In-Memory Repository Implementation:

Product In-Memory Repository Implementation:

And the use cases for cart and user as written below :

User Usecase Port:

User Usecase Interactor:

Cart Usecase Port:

Cart Usecase Interactor:

Product Usecase Interactor:

Last but not least, let’s create a small CLI program to test our shopping cart functionalities that we’ve created so far.

Shopping Cart CLI tester using in-memory repository:

Ok, some of you might get overwhelmed with all of the code here (while some of you might not), especially in regards with the use case port and interactor (what would be the difference between those 2).

Test our CLI by using this following command from the terminal:

go run cmd/cli/main.go

You can grab the full code from here

Let me know for any concerns and/or questions from you guys :-).

Top comments (0)