Oluwatosin Oghenewaire Thompson
goninja

goninja

GO - Following the Domain-Driven Design Approach

GO - Following the Domain-Driven Design Approach

Oluwatosin Oghenewaire Thompson's photo
Oluwatosin Oghenewaire Thompson
·Nov 30, 2021·

5 min read

Table of contents

  • Domain-Driven Design
  • Why you should use DDD
  • REFERENCES

Do you have a large codebase and it's been difficult to organize? Do you think a new team or team member can navigate through your codebase easily? If your answer is no, then the solution is here.

You should have a basic understanding of GoLang. You should understand concepts like the receiver function and interface implementation in GoLang. Interfaces in golang. Reciever functions in golang.

Domain-Driven Design

This is an approach to programming that eliminates the difficulties of structures. As your codebase increases, it becomes more and more difficult to structure and maintain it - this is inevitable. DDD helps to eliminate this problem by following a structure that allows the constant evolution of your code without breaking it.

Why you should use DDD

  • Business and development are aligned - the technical and the business experts align in the decision making and the same domain pattern created are followed.

  • Provides patterns to solve difficult problems

  • Complex designs are based on the domain model

  • Flexibility - when the domain requirement changes, you can easily make changes in your code

  • Better Code

Okay, before we dive in, let's have a brief overview of the terminologies used in DDD...

DDD uses a layered architecture to separate concerns and prevent chaos by splitting the codebase into 4 main layers. In my root folder, I would create four (4) directories or packages that would help us to combine and integrate the application. I will name them DOMAIN, INFRASTRUCTURE , APPLICATION, and INTERFACES.

In the Domain layer, I will create 2 new directories and name them ENTITY, and REPOSITORY. The entity package will contain all the business models and will use ubiquitous language. The schema of the things is defined in the entity package. The repository package defines the different methods that the infrastructure will implement. It defines the number of methods that interacts with a given database.

The Infrastructure layer implements all the methods defined in the repository package (within the domain layer). All methods and logic that involves interacting with the database (persistence), messaging and communication between the different layers are defined here.

The Application layer assigns domain objects to perform their jobs.

The Interface layer is responsible for displaying data and listening to events as they occur. This is the layer that handles all HTTP requests and responses.

The example template will create a country with the states within them.

domain-driven-design.png

define each entity in its file within the entity package.

define each entity repository in its file within the repository package.

define each entity application in its file within the application package

define each entity interface in its file interface package

you can have - entity => country.go | repository => dbcountry.go | application => country.go | interface => country.go

(country.go)

package entity

//define the model

type CountryStruct struct {
     Name string `json:"name"`
     CountryCode string `json:"code"`
     Francophone bool `json:"francophone"`
     Anglophone bool `json:"anglophone"`
}
(dbcountry.go)

package repository
import (
    "presentation/domain/entity"
)

//define the methods of the interface. 
//all the methods that interact with the database are defined here
//for example, all crud operations that belong to the country entity
//are defined under this interface

type CountryRepository interface {
    CreateCountry (entity.CountryStruct) (interface{}, error)
     //ReadCountry(entity.CountryStruct) (interface{}, error)
     //UpdateCountry(string, entity.CountryStruct) (interface{}, error)
     //DeleteCountry(string, entity.CountryStruct) (interface{}, error)
}
(db.go)

package persistence

import (
    "errors"
    "fmt"
    "os"
    "presentation/domain/entity"
    "presentation/domain/repository"

    "github.com/jinzhu/gorm"
    "github.com/joho/godotenv"
    _ "github.com/lib/pq" //postgres driver
)


type Repositories struct {
    Country repository.CountryRepository
    db   *gorm.DB
}
var Conn *gorm.DB

func ConnUsers() (Repositories, error) {
    fmt.Println("Attempting to connect to 'domaindriven' database...")
    err := godotenv.Load("config.env")
    if err != nil {
        err = errors.New("error accessing config file")
        fmt.Println(err)
    }

    username, password, dbName, dbHost, err := getDatabaseCredentials()
    if err != nil {
        fmt.Println(err)
    }

    dbURL := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password)
    db, err := gorm.Open("postgres", dbURL)
    if err != nil {
        fmt.Println(err)
        fmt.Println("postgres")
    }

    Conn = db
    fmt.Println("Core database connection successful")
    autoMigrateTables()
    return Repositories{
        Country: NewCountryInfra(db),
        db:   db,
    }, nil
}

func getDatabaseCredentials() (string, string, string, string, error) {
    _ = godotenv.Load("config.env")
    appMode := os.Getenv("app_mode")

    if appMode == "dev" {
        _ = godotenv.Load("dev-config.env")

        username := os.Getenv("db_user")
        password := os.Getenv("db_pass")
        dbName := os.Getenv("db_name")
        dbHost := os.Getenv("db_host")

        return username, password, dbName, dbHost, nil
    }
    errorString := errors.New("error getting environment")
    return errorString.Error(), errorString.Error(), errorString.Error(), errorString.Error(), errorString
}

func autoMigrateTables() {
    Conn.AutoMigrate(&entity.CountryStruct{})
}
package persistence

import (
    "errors"
    "presentation/domain/entity"
    "presentation/domain/repository"

    "github.com/jinzhu/gorm"
)

type CountryInfra struct {
    database *gorm.DB
}

func NewCountryInfra(database *gorm.DB) *CountryInfra {
    return &CountryInfra{database}
}

//CountryInfra implements the repository.CountryRepository interface
var _ repository.CountryRepository = &CountryInfra{}

func (r *CountryInfra) CreateCountry(c entity.CountryStruct) (interface{}, error) {
    if c.Name == "" {
        return entity.CountryStruct{}, errors.New("invalid input")
    }
    r.database.Create(c)
    return c, nil
}
package application

import (
    "presentation/domain/entity"
    "presentation/domain/repository"
)

type CountryApp struct {
    theCountry repository.CountryRepository
}

var _CountryApplication = &CountryApp{}

type CountryApplication interface {
    CreateCountry (entity.CountryStruct) (interface{}, error)
    //ReadCountry(entity.CountryStruct) (interface{}, error)
    //UpdateCountry(string, entity.CountryStruct) (interface{}, error)
    //DeleteCountry(string, entity.CountryStruct) (interface{}, error)
}

func (c *CountryApp) CreateCountry(country entity.CountryStruct) (interface{}, error) {
    return c.theCountry.CreateCountry(country)
}
package interfaces

import (
    "encoding/json"
    "errors"
    "net/http"
    "presentation/application"
    "presentation/domain/entity"
)

type CountryInterface struct {
    cu application.CountryApplication
}

func NewCountry(cu application.CountryApplication) CountryInterface {
    return CountryInterface{
        cu: cu,
    }
}

func (cu *CountryInterface) CreateCountry(w http.ResponseWriter, r *http.Request) {
    var country entity.CountryStruct

    json.NewDecoder(r.Body).Decode(&country)
    newCountry, err := cu.cu.CreateCountry(country)
    if err != nil {
        responseError := errors.New("unable to register country")
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(responseError)
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newCountry)
}

In conclusion, domain-driven design is an efficient way to manage your codebase by separating it into well-organized models that follow the laid down business logic. To efficiently use the domain-driven approach, you should have a business model.

This is not an article that explains the "ideal" way to implement DDD in Golang because I am in no way an expert on it. This article is rather my understanding of DDD based on my research. I will be very grateful for your contributions on how to improve this article.

Check the GitHub repo for the updated code: domain-driven-design

I hope this was helpful. If you have any questions or contributions, don't hesitate to drop your comments in the comment section.

REFERENCES

 
Share this