Composition Pattern in Golang by Practice
Learn how to implement the composition pattern in Go to enhance maintainability and testability of your software using dependency injection and interface definition.
17 mars 2024
Published
Hugo Mufraggi
Author

Composition Pattern In Golang By Practice
I assume you have already developed in Golang or will soon start.
Due to the language design, Go teams enforce a specific approach to software development. This is the language’s most compelling aspect. It’s easy to learn and efficient, and it encourages thoughtful design.
In this article, I will demonstrate how to architect your software to enhance maintainability and testability. I will leverage dependency injection, interface definition, and the composition pattern to achieve this.
Software Architecture
I will create a very classical software architecture. I will explain my architecture from the bottom to the top.
At the deepest level, we find:
- The DB repository, which is a structure containing all functions to interact with the database for a specific entity.
- The HTTP client, which consolidates all logic for client-side calls. It is a structure with all the functions for making calls and deserializing the response.
Middle layer
- Here, we introduce the service layer. Thanks to this layer, we can split the logic for reading and writing into two sub-structures. We inject the DB repository into the read service and the HTTP client into the write service.
Top Level
- At the top level, we have the handler, where we inject the service.

Usually, you are precise in asking if the service could be split into two sub-services. I see many benefits:
Modularity: Composition allows dividing a system into small, independent modules, making code management and maintenance easier.
Reusability: With composition, objects can be reused in different contexts without modification.
Encapsulation: Composing objects hides internal details, promoting encapsulation and reducing system complexity.
Flexibility: By combining different objects, new functionalities can be created without altering existing objects.
Low dependencies: Unlike inheritance, composition tends to create weaker dependencies between classes, making the code more flexible and less prone to side effects.
Practice Time
Project Setup
Open a new folder and create each file.
go mod init my-package
touch main.go
mkdir src
cd src
mkdir handlers repositories service utils
cd handlers
touch handler.go
cd ..
cd repositories
touch repository.go
cd ..
cd service
touch service.go write_service.go read_service,go
cd ..
cd utils
touch http_client.go
cd ..
go get github.com/google/uuid
I start by writing my repository and my HTTP client.
Repository
The most essential thing in this snippet of code is that my struct repository is private. I can initialize my repository only with the NewRepository function. The NewRepository function does not directly return my struct, but it returns the interface of my struct. The interface acts like a contract.
type IRepository interface {
Insert(pokeName string) uuid.UUID
GetNameById(id uuid.UUID) (string, error)
}
type repository struct {
pokeMap map[uuid.UUID]string
}
func NewRepository(pokeMap map[uuid.UUID]string) IRepository {
return &repository{pokeMap: pokeMap}
}
func (r *repository) Insert(pokeName string) uuid.UUID {
id := uuid.New()
r.pokeMap[id] = pokeName
return id
}
func (r *repository) GetNameById(id uuid.UUID) (string, error) {
v, exists := r.pokeMap[id]
if !exists {
return "", fmt.Errorf("not found")
}
return v, nil
}
HTTP Client
For the client, the same pattern is used with IClientPokemonHttp and NewClientPokemonHttp.
type IClientPokemonHttp interface {
GetPokemonInfo() string
}
type clientHttp struct {
url string
}
func NewClientPokemonHttp(url string) IClientPokemonHttp {
return &clientHttp{url: url}
}
func (c *clientHttp) GetPokemonInfo() string {
return "pika"
}
We have defined the deepest layer, and now I will create the service.
Service
First, we want to define the readService and the writeService. The architecture is the same as in the previous files.
readService
type IReadService interface {
GetPokemonNameById(uuid uuid.UUID) (string, error)
}
type serviceRead struct {
repository repositories.IRepository
}
func (s *serviceRead) GetPokemonNameById(id uuid.UUID) (string, error) {
name, err := s.repository.GetNameById(id)
if err != nil {
return "", err
}
return name, nil
}
func NewReadService(repository repositories.IRepository) IReadService {
return &serviceRead{repository: repository}
}
insertService
type IWriteService interface {
SavePokemon(uuid uuid.UUID) uuid.UUID
}
type serviceWrite struct {
repository repositories.IRepository
client utils.IClientPokemonHttp
}
func (s *serviceWrite) SavePokemon(id uuid.UUID) uuid.UUID {
pokeName := s.client.GetPokemonInfo()
id := s.repository.Insert(pokeName)
return id
}
func NewWriteService(repository repositories.IRepository,
client utils.IClientPokemonHttp) IWriteService {
return &serviceWrite{repository: repository, client: client}
}
Now that we have created the 2 sub-services, we can create the main service and utilize the composition system.
Service
type IService interface {
IReadService
IWriteService
}
type serviceMain struct {
IReadService
IWriteService
}
func NewMainService(repository repositories.IRepository, client utils.IClientPokemonHttp) IService {
write := NewWriteService(repository, client)
read := NewReadService(repository)
return &serviceMain{
IReadService: read, IWriteService: write}
}
In Go, the composition can be understood as merging all interfaces or structs together. We merge the IReadService and IWriteService interfaces inside IService, and IReadService and IWriteService inside serviceMain.
Afterward, it is very similar to the other structs. I have a public function to return an IService.
The last step before the main function is creating the handlers, where we inject the service inside.
type IHandler interface {
Insert() uuid.UUID
GetNameById(id uuid.UUID) (string, error)
}
type handler struct {
service service.IService
}
func (h *handler) Insert() uuid.UUID {
return h.service.SavePokemon()
}
func (h *handler) GetNameById(id uuid.UUID) (string, error) {
return h.service.GetPokemonNameById(id)
}
func NewHandler(service service.IService) IHandler {
return &handler{service: service}
}
Now, I’m creating the main function and applying dependency injection. Feel free to replace it pokeMap with a real database client. I use a map as an example to reduce the complexity of the codebase.
func main() {
pokeMap := map[uuid.UUID]string{}
repo := repositories.NewRepository(pokeMap)
client := utils.NewClientPokemonHttp("pokemon_api_url")
service := service2.NewMainService(repo, client)
handler := handlers.NewHandler(service)
id := handler.Insert()
pokeName, _ := handler.GetNameById(id)
fmt.Printf("%s\n", pokeName)
}
Conclusion
This article can assist you in producing better code in Golang. Although not too difficult to deploy, this approach can significantly impact the quality of your codebase. I didn’t cover it in this article, but the architecture is straightforward to test with unit tests. Perhaps I’ll cover testing in the following article.