Go Echo Framework + DDD + CQRS: Part 1

Ivan Cherkasov
8 min readFeb 21, 2024

--

In this series of articles, I want to share my experience in creating applications in the Golang language using the Echo Framework and principles such as DDD (Domain-Driven Design) and CQRS (Command Query Responsibility Segregation). The choice of these technologies and approaches is not accidental. Golang is known for its efficiency and simplicity, Echo Framework adds ease and speed to the development of web applications, and DDD and CQRS allow building scalable and maintainable systems.

In practice, I often encountered that not all developers fully understand the principles of DDD, especially when it comes to layer separation. A common mistake is the lack of clear differentiation between the domain layer and the infrastructure layer, where domain entities act as models of the infrastructure layer.

In this article, we will focus on practical implementation: organizing directories and creating an HTTP server for our project — a backend for an application. I will not delve into the theoretical explanation of DDD and CQRS, as much has already been written about these approaches, and the information is easily accessible.

Directory Organization

First of all, let’s consider the structure of the directories of our future application. This structure will help us organize the code in a way that adheres to the principles of DDD and CQRS.

## Project Structure


```
.
├── cmd # Command line interface and main application entry points.
│ └── myapp # Main application directory.
│ ├── cli # CLI-specific code, such as command definitions and parsers.
│ │ ├── cli.go # CLI tool setup and command configuration.
│ └── main.go # Entry point of the application.
├── internal # Application's internal codebase, not accessible from outside.
│ ├── application # Application layer: orchestrates the application flow, configuration, and CQRS implementation.
│ │ ├── app.go # Application configuration and initialization.
│ │ ├── command # Command side of CQRS: handles the execution of commands.
│ │ │ └── command.go # Command handling (CQRS).
│ │ └── query # Query side of CQRS: handles data retrieval requests.
│ │ └── query.go # Query handling (CQRS).
│ ├── domain # Core domain logic: entities, aggregates, services, and repository interfaces.
│ │ ├── aggregate # Domain aggregates, representing collections of entities that are processed together.
│ │ ├── entity # Domain entities, the fundamental objects of the business context.
│ │ ├── repository # Domain repository interfaces, abstract definitions for data access layers.
│ │ └── service # Domain services, containing business logic that doesn't naturally fit within an entity or aggregate.
│ ├── genmocks.go # Mock generation for testing, facilitating unit and integration testing.
│ ├── infrastructure # Infrastructure layer: frameworks, drivers, and tools for technical capabilities.
│ │ ├── api # API interfaces, particularly HTTP for web interaction.
│ │ │ └── rest # REST-specific implementations: servers, handlers, middleware.
│ │ │ ├── handler # REST handlers, processing incoming HTTP requests.
│ │ │ ├── middleware # REST middleware, intercepting requests for cross-cutting concerns like logging, authentication.
│ │ │ └── validator # REST request validation, ensuring requests meet the expected format.
│ │ ├── decorator # Decorators for enhancing or altering behavior (e.g., logging, metrics).
│ │ │ ├── decorator.go # Base decorators, potentially for cross-cutting concerns.
│ │ │ └── logging.go # Logging decorator, adding logging capabilities to operations.
│ │ └── pgsql # PostgreSQL implementation: models and repositories for the database.
│ │ ├── model # Data models for PostgreSQL, representing the database structure in code.
│ │ └── repository # Domain repository implementation for PostgreSQL, concrete data access operations.
│ └── mocks # Mocks for testing, automatically generated or manually crafted stubs for unit tests.
└── pkg # Shared libraries and utilities, potentially reusable across different projects.


```

This project structure is available on my GitHub: https://github.com/i4erkasov/go-ddd-cqrs

Implementing HTTP Server Using Echo Framework

To start developing our HTTP server, the first step is to create a `main.go` file in the `./cmd/myapp` directory. For now, we’ll leave this file unchanged and focus on the main task.

Echo Framework serves as the basis for our HTTP server. In the context of Domain-Driven Design (DDD), the HTTP server falls within the infrastructure layer. This means it facilitates interaction between the outside world and our application, acting as a connecting link.

To organize the server code, we create the following directory structure in `./internal/infrastructure/api/rest`:

- `server.go` — server implementation
- `route.go` — routes implementation

In the `handler`, `middleware`, and `validator` directories, we place the corresponding components:

- **Handler** (`handler/handler.go`): The main file for handlers, where each handler is responsible for performing a specific action in response to HTTP requests.

- **Middleware** (`middleware/middleware.go`): A file containing middleware, for example, for logging or authentication checks.

- **Validator** (`validator/validator.go`): Here are custom validators for checking incoming HTTP requests for compliance with certain requirements.

The directory structure of `rest` looks as follows:

.
├── handler
│ └── handler.go
├── middleware
│ └── middleware.go
├── routes.go
├── server.go
└── validator
└── validator.go

Let’s add a structure and constructor to our handler.go (we will expand it in the course of our project implementation)

package handler

type Handler struct {}

func New() *Handler {
return &Handler{}
}

Similarly, we’ll do the same with `middleware.go`.

package middleware

type Middleware struct {}

func New() *Middleware {
return &Middleware{}
}

`validator.go`
As a validator, we will use the package https://github.com/go-playground/validator. However, to use it with our Echo Framework, it is necessary to adapt it to the `echo.Validator` interface.

We will achieve this through a wrapper.

package validator

import (
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)

// wrapper implementation.
type wrapper struct {
validator *validator.Validate
}

func New() echo.Validator {
return &wrapper{
validator: validator.New(),
}
}

// Validate data
func (v *wrapper) Validate(i interface{}) error {
return v.validator.Struct(i)
}

Now let’s proceed directly to the implementation of `server.go`.

Jumping ahead, I’ll mention that in our server implementation we will be using the following packages:

- For configuration: https://github.com/spf13/viper
- For logging: https://go.uber.org/zap

The structure of our Server:

type Server struct {
echo *echo.Echo
log *zap.Logger
cfg *viper.Viper
}

We will need two public methods: `New`, our constructor, and `Start`, which will be responsible for launching our server. But before that, we need to configure our server, and here the Viper package will come in handy.

Let’s create a directory at the root of the project called `.bin`, and within it, let’s create our configuration file `config.dev.yaml`, adding our settings to it.

app:
environment: "development"
api:
rest:
host: "127.0.0.1"
port: "8088"
setting:
debug: true
hide_banner: false
hide_port: false

Before we proceed with implementing our methods for the server, let’s also define our `route.go`.

package rest

import (
"net/http"

"github.com/labstack/echo/v4"
)

func (s *Server) routes(h *handler.Handler, m *middleware.Middleware) {

}

Now that we have everything we need, we can start implementing the server. For the sake of code readability, I’ve divided the implementation into several private methods: `start`, `configure`, and `handleErrors`. Here’s what we ended up with.

package rest

import (
"context"
"net"
"net/http"

"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/handler"
"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/middleware"
"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/validator"
"github.com/juju/errors"
"github.com/labstack/echo/v4"
"github.com/spf13/viper"
"go.uber.org/zap"
)

type Server struct {
echo *echo.Echo
log *zap.Logger
cfg *viper.Viper
}

func New(cfg *viper.Viper, log *zap.Logger) (*Server, error) {
server := &Server{
echo: echo.New(),
log: log,
cfg: cfg,
}

server.configure(cfg.Sub("setting"))

server.routes(
handler.New(),
middleware.New(),
)

return server, nil
}

func (s *Server) Start(ctx context.Context) error {
errorChan := make(chan error, 1)


go s.start(errorChan)

select {
case <-ctx.Done():
s.log.Info("Shutting down the server")
if shutdownErr := s.echo.Shutdown(ctx); shutdownErr != nil {
s.log.Error("Error shutting down the server", zap.Error(shutdownErr))
return shutdownErr
}
case err := <-errorChan:
s.log.Fatal("Failed to start HTTP server", zap.Error(err))
return err
}

return nil
}

func (s *Server) start(errorChan chan<- error) {
defer close(errorChan)


if err := s.echo.Start(
net.JoinHostPort(
s.cfg.GetString("host"),
s.cfg.GetString("port"),
),
); err != nil && !errors.Is(err, http.ErrServerClosed) {
errorChan <- err
}
}

func (s *Server) configure(cfg *viper.Viper) {
if cfg.IsSet("debug") {
s.echo.Debug = cfg.GetBool("debug")
}

if cfg.IsSet("hide_banner") {
s.echo.HideBanner = cfg.GetBool("hide_banner")
}

if cfg.IsSet("hide_port") {
s.echo.HidePort = cfg.GetBool("hide_port")
}

s.echo.Validator = validator.New()
s.echo.HTTPErrorHandler = handleErrors(s.log, cfg.GetBool("debug"))
}

func handleErrors(log *zap.Logger, debug bool) echo.HTTPErrorHandler {
return func(err error, c echo.Context) {
var (
code = http.StatusInternalServerError
msg string
errorStack any
)

if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
msg = he.Message.(string)
} else {
msg = err.Error()
switch true {
case errors.Is(err, errors.BadRequest):
code = http.StatusBadRequest
case errors.Is(err, errors.Forbidden):
code = http.StatusForbidden
case errors.Is(err, errors.Unauthorized):
code = http.StatusUnauthorized
case errors.Is(err, errors.NotFound):
code = http.StatusNotFound
case errors.Is(err, errors.AlreadyExists):
code = http.StatusConflict
}

if debug {
errorStack = errors.ErrorStack(err)
}
}

if !c.Response().Committed {
if err != nil && code == http.StatusInternalServerError {
log.Error("An error occurred", zap.Error(err))
}

if c.Request().Method == echo.HEAD {
err = c.NoContent(code)
} else {
m := echo.Map{"error": msg}
if errorStack != nil {
m["errorStack"] = errorStack
}
err = c.JSON(code, m)
}
}
}
}

Let’s create our first handler in the `handler` directory and, following tradition, name it `hello_world.go`.

package handler

import (
"net/http"

"github.com/labstack/echo/v4"
)

func (h *Handler) HelloWold(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}

Then, we will add a route for our request handler. We will add the corresponding code to the `route.go` file.

package rest

import (
"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/handler"
"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest/middleware"
)

func (s *Server) routes(h *handler.Handler, m *middleware.Middleware) {
s.echo.GET("/hello", h.HelloWold)
}

Run the Server

Launch the Server

Now that our server is ready, let’s implement a command to start it. For the CLI functionality, we will use the package [https://github.com/spf13/cobra](https://github.com/spf13/cobra). In the directory `./cmd/myapp/cli`, create a file named `cli.go`.

package cli

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
config string
cfg *viper.Viper
)

var cmd = &cobra.Command{
Use: "cmd",
Short: "ShortDescription ",
Long: `Long Description`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
cfg = viper.New()
cfg.AddConfigPath(".")
cfg.AutomaticEnv()
cfg.SetConfigFile(config)
return cfg.ReadInConfig()
},
}

func Execute() error {
return cmd.Execute()
}

func init() {
cmd.PersistentFlags().StringVarP(
&config, "config", "c", "config.yml",
"path to config file",
)
}

Next, in the same directory, create a file named `http_server.go` and implement a command to launch the server. Note that in this example, the code for creating the logger is temporary and will be replaced in the future.

package cli


import (
"os"
"time"

"github.com/i4erkasov/go-ddd-cqrs/internal/infrastructure/api/rest"
"github.com/spf13/cobra"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const HttpServerCommand = "http-server"
const VersionHttpServer = "1.0.0"

var httpServer = &cobra.Command{
Use: HttpServerCommand,
Short: "Start http server",
Version: VersionHttpServer,
RunE: func(cmd *cobra.Command, args []string) (err error) {
bws := &zapcore.BufferedWriteSyncer{
WS: os.Stderr,
Size: 512 * 1024,
FlushInterval: time.Minute,
}
defer bws.Stop()
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core := zapcore.NewCore(consoleEncoder, bws, zapcore.DebugLevel)
log := zap.New(core)

cnf := cfg.Sub("app.api.rest")

var server *rest.Server
if server, err = rest.New(cnf, log); err != nil {
return err
}

return server.Start(cmd.Context())
},
}

func init() {
cmd.AddCommand(httpServer)
}

Finally, in the `./cmd/myapp/main.go` file, which we created at the very beginning, add the following code:

package main

import (
"fmt"
"os"

"github.com/i4erkasov/go-ddd-cqrs/cmd/myapp/cli"
)

func main() {
if err := cli.Execute(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Some error occurred during execute app. Error: %v\n", err)

os.Exit(2)
}
}

After everything is set up, try to launch the server. Navigate to the directory where our `main.go` file is located, specifically `./cmd/myapp`, and execute the command `go run main.go -c <path to the configuration file>`.

My command looks like this:

go run main.go http-server -c ~/go/src/github.com/i4erkasov/go-ddd-cqrs/.bin/config.dev.yaml

You should see a message in the console.

The server is successfully launched, and now we can go to the browser at http://127.0.0.1:8088/hello and see the server in action.

Conclusions and Next Steps

In this article, we explored the organization of directories for our project and started implementing an HTTP REST server. These steps are an important foundation for further development of our application in accordance with DDD and CQRS principles. In the next article, we will add a command for working with migrations, examine the application layer, and discuss why a `pkg` directory is needed.
Everything implemented in this article can be found on GitHub: https://github.com/i4erkasov/go-ddd-cqrs/tree/part-1

Part 1.1 >>

--

--