Titus Dishon
6 Oct 2021
•
8 min read
In this article, I will be using the above model to implement Domain-driven design with the go programming language. What is domain-driven design?
Domain-driven design involves dividing the whole model into smaller easy to manage and change sub-models. In our case the process of adding a new user to the system has four sub-layers:
Data access objects(DAO)
: This layer contains the basic CRUD operations for one entity class. Data Transfer Objects
: Objects that mirror database schemes. Here we define our entities and their data structure.Why should I use the structure above? Closely observing the structure, you will realize that we can change the web framework in use without touching the domain and services layer, you can as well change the datastore in use by changing the Domain layer-> DAO file. When one is required to change only some business logic, they will have to change the files in the services layer only. This makes the code to be maintainable and easy to scale up especially when the maintainer is different from the original author.
Prerequisites for the project:
With all that in mind let's start to create the authentication system.#### Project structure
git mod init example.com
This will create a go.mod file which contains the list of packages you will install on your project.
-Create other two files:
- `Dockerfile `: For docker configurations
- `docker-compose.yaml`: For docker-compose configurations, docker-compose will be used to run the project as it abstracts the different docker commands with simple ones.
Paste the code below in the Dockerfile
FROM golang1.16
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
CMD ["air"]
and for the docker-compose.yaml
file, paste the code below.
version: "3.9"
services:
go-domain-driven-dev-auth-service:
build: .
ports:
- "9000:9000"
volumes:
- .:/app
depends_on:
- db
# database connection and creation
db:
image: mysql:5.7.22
build: ./mysql-db
restart: always
environment:
MYSQL_DATABASE: go-domain-driven-dev-auth-service
MYSQL_USER: root
MYSQL_PASSWORD: root
MYSQL_ROOT_PASSWORD: root
volumes:
- .dbdata:/var/lib/mysql
# map mysql port to a different port
ports:
- "33066:3306"
Create a folder databasein the root of the project, and add three files :
connection.go
: Will connect us to the database.
package database
import (
"database/sql"
"os"
_ "github.com/go-sql-driver/mysql"
)
var MYDB *sql.DB
func ConnectToMysql() {
var err error
dsn := os.Getenv("MYSQLDB")
MYDB, err = sql.Open("mysql", dsn)
if err != nil {
panic("Could not connect to mysql database!!")
}
}
create a .env file in your project root directory and add environment variables
MYSQLDB=username:password@tcp(docker_container_name:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local
SECRET_KEY=somesecretkeyhere
Create a folder middlewares the root of the project, and add file :
auth_middleware.go
: Will contain authentication middlewares like a function to generate jwt token and check authentication status.
auth_middleware.go
package middlewares
import (
"github.com/dgrijalva/jwt-go"
"github.com/gofiber/fiber/v2"
"os"
"time"
)
type Claims struct {
Email string `json:"email,omitempty"`
Scope string
jwt.StandardClaims
}
func GenerateJWT(id int, email string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &Claims{
Email: email,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Subject: string(id),
},
})
return token.SignedString([]byte(os.Getenv("SECRET_KEY")))
}
func IsAuthenticated(c *fiber.Ctx) error {
cookie := c.Cookies("jwt")
token, err := jwt.ParseWithClaims(cookie, &Claims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("SECRETLY")), nil
})
if err != nil || !token.Valid {
c.Status(fiber.StatusUnauthorized)
return c.JSON(
fiber.Map{
"message": "unauthenticated",
})
}
return c.Next()
}
Create a folder domain in the root of the project, and add three files :
user_dao.go
: Will contain the methods to act on entities.
user_dto.go
: will contain our entity definition.
marshal.go
: This will be used to return a list of users. It can also be used to differentiate a private user data object from a public user data object.
Paste the code below to the user_dto.go
file and the routes.go files:
user_dto.go
package domain
import (
"golang.org/x/crypto/bcrypt"
"strings"
"time"
)
type User struct {
ID int `json:"id"`
FullName string `json:"full_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Password []byte `json:"-"`
DateCreated time.Time `json:"date_created"`
DateModified time.Time `json:"date_modified"`
}
type Users []User
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"-"`
}
// SetPassword: sets the hased password to the user struct defined above
func (user *User) SetPassword(password string) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(strings.TrimSpace(password)), 12)
user.Password = hashedPassword
}
// ComparePassword: Used to compare user stored password and login password
func (user *User) ComparePassword(password string) error {
return bcrypt.CompareHashAndPassword(user.Password, []byte(strings.TrimSpace(password)))
}
In the user_dao.go
paste the code below:
package domain
import (
"database/sql"
"go-domain-driven-dev-auth-service/database"
"log"
)
var (
createUserQuery = `INSERT INTO users(full_name, email, phone_number, password, date_created) VALUES(?,?,?,?,?)`
getUserQuery = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user WHERE user.id=?`
getAllUsersQuery = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user`
updateUserQuery = `UPDATE users SET full_name=?, email=?, phone_number=?, date_modified=? WHERE user.id=?`
getUserByEmailAndPasswordQuery = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user WHERE user.email=?`
)
func (user *User) Save() error {
stmt, err := database.MYDB.Prepare(createUserQuery)
if err != nil {
return err
}
defer func(stmt *sql.Stmt) {
err := stmt.Close()
if err != nil {
log.Fatalf(err.Error())
}
}(stmt)
res, saveErr := stmt.Exec(
user.FullName,
user.Email,
user.PhoneNumber,
user.Password,
user.DateCreated)
if saveErr != nil {
return err
}
userId, err := res.LastInsertId()
if err != nil {
log.Printf("Error when getting the last inserted id for user %s", err)
return err
}
user.ID = int(userId)
return nil
}
func (user *User) Get() error {
stmt, err := database.MYDB.Prepare(getUserQuery)
if err != nil {
return err
}
defer func(stmt *sql.Stmt) {
err := stmt.Close()
if err != nil {
log.Fatalf(err.Error())
}
}(stmt)
result := stmt.QueryRow(user.ID)
if getErr := result.Scan(
&user.FullName,
&user.Email,
&user.PhoneNumber,
&user.DateCreated,
&user.DateModified,
&user.ID); getErr != nil {
return getErr
}
return nil
}
func (user *User) GetUsers() ([]User, error) {
stmt, err := database.MYDB.Prepare(getAllUsersQuery)
if err != nil {
return nil, err
}
defer func(stmt *sql.Stmt) error {
err := stmt.Close()
if err != nil {
return err
}
return nil
}(stmt)
rows, err := stmt.Query()
if err != nil {
return nil, err
}
defer func(rows *sql.Stmt) {
err := rows.Close()
if err != nil {
log.Fatalf(err.Error())
}
}(stmt)
results := make([]User, 0)
for rows.Next() {
var user User
if err := rows.Scan(
user.FullName,
user.Email,
user.PhoneNumber,
user.DateCreated,
user.DateModified,
user.ID); err != nil {
return nil, err
}
results = append(results, user)
}
if len(results) == 0 {
return nil, err
}
return results, nil
}
func (user *User) Update() error {
stmt, err := database.MYDB.Prepare(updateUserQuery)
if err != nil {
return err
}
defer func(stmt *sql.Stmt) {
err := stmt.Close()
if err != nil {
log.Fatalf(err.Error())
}
}(stmt)
res, saveErr := stmt.Exec(
user.FullName,
user.Email,
user.PhoneNumber,
user.Password,
user.DateModified)
if saveErr != nil {
return err
}
userId, err := res.LastInsertId()
if err != nil {
log.Printf("Error when getting the last inserted id for user %s", err)
return err
}
user.ID = int(userId)
return nil
}
func (user *User) FindByEmailAndPassword() error {
stmt, err := database.MYDB.Prepare(getUserByEmailAndPasswordQuery)
if err != nil {
return err
}
defer func(stmt *sql.Stmt) {
err := stmt.Close()
if err != nil {
log.Fatalf(err.Error())
}
}(stmt)
result := stmt.QueryRow(user.Email)
if getErr := result.Scan(
&user.FullName,
&user.Email,
&user.PhoneNumber,
&user.Password,
&user.ID); getErr != nil {
log.Fatalf("Error trying to get user by email and password")
return err
}
return nil
}
marshal.go
package domain
import (
"time"
)
type PrivateUser struct {
ID int `json:"id"`
FullName string `json:"full_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Password []byte `json:"-"`
DateCreated time.Time `json:"date_created"`
DateModified time.Time `json:"date_modified"`
}
type PublicUser struct {
FullName string `json:"full_name"`
}
func (users Users) Marshall() interface{} {
result := make([]interface{}, len(users))
for index, user := range users {
result[index] = user
}
return result
}
Create a folder service in the root of the project, and add files:
user_services.go
: Will contain the business logic for the user.
user_services.go
package services
import "go-domain-driven-dev-auth-service/domain"
type authenticationInterface interface {
CreateUser(user domain.User) (*domain.User, error)
GetUser(userId int) (*domain.User, error)
GetUsers() (domain.Users, error)
UpdateUser(user domain.User) (*domain.User, error)
Login(loginRequest domain.LoginRequest) (*domain.User, error)
}
type authService struct{}
var (
AuthService authenticationInterface = &authService{}
)
func (a authService) CreateUser(user domain.User) (*domain.User, error) {
user.SetPassword(string(user.Password))
if err := user.Save(); err != nil {
return nil, err
}
return &user, nil
}
func (a authService) GetUser(userId int) (*domain.User, error) {
result := &domain.User{ID: userId}
if err := result.Get(); err != nil {
return nil, err
}
return result, nil
}
func (a authService) GetUsers() (domain.Users, error) {
results := &domain.User{}
return results.GetUsers()
}
func (a authService) UpdateUser(user domain.User) (*domain.User, error) {
if err := user.Update(); err != nil {
return nil, err
}
return &user, nil
}
func (a authService) Login(loginRequest domain.LoginRequest) (*domain.User, error) {
dao := &domain.User{
Email: loginRequest.Email,
Password: []byte(loginRequest.Password),
}
if err := dao.FindByEmailAndPassword(); err != nil {
return nil, err
}
return dao, nil
}
Create a folder service in the root of the project, and add files:
user_services.go
: Will contain the business logic for the user.
user_services.go
package controllers
import (
"github.com/gofiber/fiber/v2"
"go-domain-driven-dev-auth-service/domain"
"go-domain-driven-dev-auth-service/middlewares"
"go-domain-driven-dev-auth-service/services"
"net/http"
"strconv"
"time"
)
type authControllerInterface interface {
CreateUser(c *fiber.Ctx) error
GetUser(c *fiber.Ctx) error
GetUsers(c *fiber.Ctx) error
UpdateUser(c *fiber.Ctx) error
Login(c *fiber.Ctx) error
}
type authControllers struct{}
var (
AuthControllers authControllerInterface = &authControllers{}
)
func (a authControllers) CreateUser(c *fiber.Ctx) error {
var user domain.User
if err:=c.BodyParser(&user); err!=nil {
return c.JSON(fiber.Map{
"message":"Invalid JSON body",
})
}
result, saveErr:= services.AuthService.CreateUser(user)
if saveErr!=nil {
return c.JSON(saveErr)
}
return c.JSON(fiber.Map{
"StatusCode": http.StatusOK,
"message": "succeeded",
"users": result,
})
}
func (a authControllers) GetUser(c *fiber.Ctx) error {
userId, err := strconv.Atoi(c.Params("user_id"))
if err!=nil {
return c.JSON(fiber.Map{
"message":"Invalid user id",
})
}
user, getErr := services.AuthService.GetUser(userId)
if getErr!=nil {
return c.JSON(getErr)
}
return c.JSON(fiber.Map{
"StatusCode": http.StatusOK,
"message": "succeeded",
"users": user,
})
}
func (a authControllers) GetUsers(c *fiber.Ctx) error {
results, getErr := services.AuthService.GetUsers()
if getErr!=nil {
return c.JSON(getErr)
}
return c.JSON(fiber.Map{
"StatusCode": http.StatusOK,
"message": "succeeded",
"users": results,
})
}
func (a authControllers) UpdateUser(c *fiber.Ctx) error {
var user domain.User
if err:=c.BodyParser(&user); err!=nil {
return c.JSON(fiber.Map{
"message":"Invalid JSON body",
})
}
result, saveErr:= services.AuthService.UpdateUser(user)
if saveErr!=nil {
return c.JSON(saveErr)
}
return c.JSON(fiber.Map{
"StatusCode": http.StatusOK,
"message": "succeeded",
"users": result,
})
}
func (a authControllers) Login(c *fiber.Ctx) error {
var loginRequest domain.LoginRequest
if err:=c.BodyParser(&loginRequest); err!=nil {
return c.JSON(fiber.Map{
"message":"Invalid JSON body",
})
}
user, getErr := services.AuthService.Login(loginRequest)
if getErr!=nil {
return c.JSON(getErr)
}
token, err := middlewares.GenerateJWT(user.ID, user.Email)
if err!=nil {
return c.JSON(fiber.Map{
"message":"We could not log you in at this time, please try again later",
})
}
cookie := fiber.Cookie{
Name: "jwt",
Value: token,
Expires: time.Now().Add(time.Hour * 24),
HTTPOnly: true,
}
c.Cookie(&cookie)
return c.JSON(fiber.Map{
"StatusCode": http.StatusOK,
"message": "succeeded",
"users":user,
})
}
Create a folder application in the root of the project, and add two files :
routes.go
: Will contain the routes configurations.
app.go
: will contain our web framework configuration, in my case fiber configuration
Paste the code below to the app.go file and the routes.go files:
app.go
package application
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
var(
router= fiber.New()
)
func StartApplication() {
router.Use(cors.New(cors.Config{
AllowCredentials: true,
}))
MapUrls()
router.Listen(":9022")
}
and in the application.go
file, paste the code below:
func MapUrls() {
api := router.Group("api")
admin := api.Group("admin")
admin.Post("register", controllers.AuthControllers.CreateUser)
admin.Post("login", controllers.AuthControllers.Login)
adminAuthenticated := admin.Use(middlewares.IsAuthenticated)
adminAuthenticated.Get("users/:userCode", controllers.AuthControllers.GetUser)
adminAuthenticated.Get("users/search-by-status", controllers.AuthControllers.GetUsers)
}
Finally, create the file server.go
in the root of your project to contain the main function:
server. go
package main
import (
"go-domain-driven-dev-auth-service/database"
"go-domain-driven-dev-auth-service/routes"
"log"
"github.com/joho/godotenv"
)
func main() {
// load environment variables
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("error loading .env file")
}
// connect to mysql
database.ConnectToMysql()
// Start the application
routes.StartApplication()
}
Install a visual database design tool like MySQL workbench on your machine and connect to your database, then create the table for users:
To fire up your applications, on your terminal change directory into the project root and run the following command.
docker-compose up
http://localhost:9000
Titus Dishon
Hello, Am Titus Dishon. A software engineer based in Nairobi Kenya. I have advanced skills in Full-stack development using Nodejs , golang, reactjs , Javascript and Typescript. I also have experience using the AWS cloud and am a holder of Cloud practitioner certification. On the database side, I have experience working with PostgreSQL , MySQL, DynamoDb and MongoDB. Over the past two years I have heavily developed micro-frontend architecture systems using nx and react. I have also been developing Microservice architecture using AWS Lambdas, knesis , SQS, SNS and DynamoDB. My fun developemtn with AWS Cloud is the concept of infrastructure as code using the SDK. For more about these experiences, feel free to reach out.
See other articles by Titus
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!