This page looks best with JavaScript enabled

Hexagonal in Go

 ·  ☕ 6 min read  ·  ✍️ t1

Hexagonal architecture, also known as ports and adapters architecture, is a software design pattern that helps to decouple the various components of a software system and make them more independent and modular. It is a way of organizing and structuring the packages of a microservice in such a way that the various components of the system can be easily plugged in and replaced without affecting the rest of the system.

In hexagonal architecture, the core business logic of the system is placed at the center, surrounded by a set of interfaces or ports that define how the business logic can be accessed and used by external components. These external components, known as adapters, are responsible for implementing the interfaces and connecting the business logic to the outside world.

Here’s an example of how the packages of a microservice might be organized and structured using hexagonal architecture in Go:

├── main.go ├── adapter │ └── http │ └── http.go ├── domain │ ├── model.go │ ├── repository.go │ ├── service.go │ └── usecase.go └── infrastructure ├── database.go ├── messaging.go ├── logger.go └── twitter ├── client.go └── client_test.go

In this example, the main.go file is the entry point of the microservice and it contains the code for starting up the server and routing incoming requests to the appropriate adapter. The adapter package contains the various adapters that connect the business logic to the outside world, such as an HTTP adapter for handling HTTP requests, a database adapter for interacting with a database, and a messaging adapter for sending and receiving messages.

The domain package contains the core business logic of the system, including the domain models, repository interfaces for storing and retrieving data, service interfaces for performing business logic, and use case interfaces for executing business logic in response to external requests.

The infrastructure package contains the implementations of the various interfaces defined in the domain package, such as concrete implementations of repositories and services that use a specific database or messaging system. It may also contain other infrastructure components such as a logger for logging messages.

By organizing and structuring the packages in this way, it becomes easier to replace or modify any component of the system without affecting the rest of the system, as the business logic is decoupled from the external components that depend on it.

In hexagonal architecture, the service and use case are two different types of components that serve different purposes.

A service is a component that contains the core business logic of the system. It is responsible for performing operations that are relevant to the domain and that have a significant impact on the state of the system. Services are typically defined as interfaces, with the actual implementation of the business logic being provided by concrete implementations of those interfaces.

A use case, on the other hand, is a component that represents a specific use of the system. It is responsible for orchestrating the flow of control and coordinating the interactions between different services and other components in order to accomplish a specific task or fulfill a specific requirement. Use cases are typically defined as interfaces, with the actual implementation of the use case being provided by concrete implementations of those interfaces.

In summary, the main difference between a service and a use case is that a service contains the core business logic of the system, while a use case represents a specific use of the system and coordinates the interactions between different components in order to accomplish a specific task.

Here’s an example of a microservice in Go that handles incoming REST API calls to retrieve Twitter feeds:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
	"context"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/pkg/errors"
	"github.com/yourusername/twitter-feed-microservice/adapter/http"
	"github.com/yourusername/twitter-feed-microservice/domain/model"
	"github.com/yourusername/twitter-feed-microservice/domain/repository"
	"github.com/yourusername/twitter-feed-microservice/domain/service"
	"github.com/yourusername/twitter-feed-microservice/infrastructure/twitter"
)

func main() {
	// Set up dependencies
	twitterClient := twitter.NewClient()
	tweetRepository := repository.NewTweetRepository(twitterClient)
	tweetService := service.NewTweetService(tweetRepository)

	// Set up HTTP router
	router := mux.NewRouter()

	router.HandleFunc("/tweets/{username}", func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		username := vars["username"]

		tweets, err := tweetService.GetTweetsForUser(context.Background(), username)
		if err != nil {
			http.Error(w, errors.Wrap(err, "failed to get tweets").Error(), http.StatusInternalServerError)
			return
		}

		http.JSON(w, http.StatusOK, tweets)
	})

	http.ListenAndServe(":8080", router)
}

In this example, the microservice has a main function that sets up the dependencies needed to retrieve tweets from Twitter, including a twitter.Client for communicating with the Twitter API, a repository.TweetRepository for storing and retrieving tweets, and a service.TweetService for performing business logic related to tweets.

The microservice also sets up an HTTP router using the gorilla/mux library and defines a handler function for the /tweets/{username} endpoint that uses the tweetService to retrieve tweets for a given username and returns them to the client as a JSON response.

This is just a simple example, and a real-world microservice might include additional features such as authentication, rate limiting, and error handling.
domain/model.go:

1
2
3
4
5
6
7
8
9
package model

// Tweet is a domain model representing a tweet.
type Tweet struct {
	ID        int64
	Username  string
	Text      string
	Timestamp string
}

domain/repository.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package repository

import (
	"context"
	"github.com/yourusername/twitter-feed-microservice/domain/model"
)

// TweetRepository is an interface that defines the methods for storing and retrieving tweets.
type TweetRepository interface {
	GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error)
}

// TweetRepositoryImpl is a concrete implementation of the TweetRepository interface that stores and retrieves tweets using a twitter.Client.
type TweetRepositoryImpl struct {
	client Client
}

// NewTweetRepository returns a new instance of TweetRepositoryImpl.
func NewTweetRepository(client Client) TweetRepository {
	return &TweetRepositoryImpl{
		client: client,
	}
}

// TweetRepositoryImpl.GetTweetsForUser retrieves the tweets for a given username using the twitter.Client.
func (r *TweetRepositoryImpl) GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error) {
	return r.client.GetTweetsForUser(ctx, username)
}

domain/service.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package service

import (
	"context"
	"github.com/yourusername/twitter-feed-microservice/domain/model"
	"github.com/yourusername/twitter-feed-microservice/domain/repository"
)

// TweetService is an interface that defines the methods for performing business logic related to tweets.
type TweetService interface {
	GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error)
}

// TweetServiceImpl is a concrete implementation of the TweetService interface.
type TweetServiceImpl struct {
	tweetRepository repository.TweetRepository
}

// NewTweetService returns a new instance of TweetServiceImpl.
func NewTweetService(tweetRepository repository.TweetRepository) TweetService {
	return &TweetServiceImpl{
		tweetRepository: tweetRepository,
	}
}

// TweetServiceImpl.GetTweetsForUser retrieves the tweets for a given username.
func (s *TweetServiceImpl) GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error) {
	return s.tweetRepository.GetTweetsForUser(ctx, username)
}

infrastructure/twitter/client.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package twitter

import (
	"context"
	"github.com/yourusername/twitter-feed-microservice/domain/model"
)

// Client is an interface that defines the methods for interacting with the Twitter API.
type Client interface {
	GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error)
}

// TwitterClient is a concrete implementation of the Client interface that uses the Twitter API to retrieve tweets.
type TwitterClient struct {
	// Add fields and methods for interacting with the Twitter API
}

// NewClient returns a new instance of TwitterClient.
func NewClient() Client {
	return &TwitterClient{
		// Initialize fields and methods
	}
}

// TwitterClient.GetTweetsForUser retrieves the tweets for a given username using the Twitter API.
func (c *TwitterClient) GetTweetsForUser(ctx context.Context, username string) ([]*model.Tweet, error) {
	// Add code for interacting with the Twitter API and retrieving tweets
	return nil, nil
}
Share on

t1
WRITTEN BY
t1
Dev