Skip to content

sebajax/go-vertical-slice-architecture

Repository files navigation

⚡ go-vertical-slice-architecture

This structure, created following the development guide's for vertical slice architecture, will help to isolate the dependencies, make development easier and have a cleaner and testable code in every package.

👩‍💻 Authors

Sebastián Ituarte

Luis Fernando Miranda

🖍 Vertical slice architecture

Vertical slice architecture is an approach to software development where code and functionality are organized around individual features or user stories, encompassing all layers of the application from user interface to data access, promoting autonomy, reduced dependencies, and iterative development.

alt text

📚 Code Structure

alt text

A brief description of the layout:

  • .github has two template files for creating PR and issue. Please see the files for more details.
  • .gitignore varies per project, but all projects need to ignore bin directory.
  • .golangci.yml is the golangci-lint config file.
  • Makefile is used to build the project. You need to tweak the variables based on your project.
  • CHANGELOG.md contains auto-generated changelog information.
  • OWNERS contains owners of the project.
  • README.md is a detailed description of the project.
  • cmd contains the main.go file that is our starting point to execute
  • pkg places most of project business logic.
  • migrations contains all vendored code.
  • internal contains all the api logic.

🚀 Stack

Programming language Go

Framework Fiber

Dependency injection Uber dig

Database Postgre SQL

Container Docker

Live reload Air

🧐 This app uses conventional commits

Conventional commits url

🤜 How to create a new use case (Example)

  POST /api/product
Parameter Type Description
name string Required. Product Name
sku string Required. Product Sku must be Unique
category string Required. Product Category
price float Required. Product Price

Internal folder structure for a new domain all folders and files go in internal/product/

├───internal
│   ├───product
│   │   │   port.go
│   │   │   product.go
│   │   │
│   │   ├───handler
│   │   │       createProduct.go
│   │   │       handler.go
│   │   │
│   │   ├───infrastructure
│   │   │       productRepository.go
│   │   │
│   │   ├───mock
│   │   │       mockProductRepository.go
│   │   │
│   │   └───service
│   │           createProduct.go
│   │           service.go

1 - Create product.go (domain)

package product
import "time"
// ProductCategory represents the categories of electronic products
type ProductCategory int
// Enumeration of product categories
const (
Laptop ProductCategory = iota
Smartphone
Tablet
SmartWatch
Headphones
Camera
Television
Other
)
// String representation of the ProductCategory
func (p ProductCategory) String() string {
return [...]string{
"Laptop",
"Smartphone",
"Tablet",
"SmartWatch",
"Headphones",
"Camera",
"Television",
"Other",
}
}
// Const for error messages
const (
ErrorSkuExists string = "ERROR_SKU_EXISTS"
ErrorWrongCategory string = "ERROR_WRONG_CATEGORY"
)
// Product Domain
type User struct {
Id int
Name string
Sku string
Category ProductCategory
Price string
CreatedAt time.Time
}
// Create a new product instance
func New(n string, s string, c string, p string) (*Product, error) {
return &Product{
Name: n,
Sku: s,
Category: c,
Price: p,
}, nil
}

2 - Create infrastructure/productRepository.go (DB query)

package infrastructure
import (
"database/sql"
"fmt"
"github.com/sebajax/go-vertical-slice-architecture/internal/product"
"github.com/sebajax/go-vertical-slice-architecture/pkg/database"
)
// Product repository for querying the database
type productRepository struct {
db *database.DbConn
}
// Create a product instance repository
func NewProductRepository(dbcon *database.DbConn) product.ProductRepository {
return &productRepository{db: dbcon}
}
// Stores a new product in the database
func (repo *productRepository) Save(p *product.Product) (int64, error) {
// Get the id inserted in the database
var id int64
query := `INSERT INTO product (name, sku, category, price)
VALUES ($1, $2, $3, $4) RETURNING id`
err := repo.db.DbPool.QueryRow(query, p.Name, p.Sku, p.Category, p.Price).Scan(&id)
if err != nil {
return 0, err
}
fmt.Println("id: ", id)
// No errors return the product id inserted
return id, nil
}
// Gets the product by the sku
func (repo *productRepository) GetBySku(sku string) (*product.Product, bool, error) {
p := product.Product{}
var category string
query := `SELECT id, name, sku, category, price, created_at
FROM product
WHERE sku = $1`
err := repo.db.DbPool.QueryRow(query, sku).Scan(&p.Id, &p.Name, &p.Sku, &category, &p.Price, &p.CreatedAt)
if err != nil {
// Not found, but not an error
if err == sql.ErrNoRows {
return nil, false, nil
}
// An actual error occurred
return nil, false, err
}
// Parse to Enum category
p.Category = product.ParseProductCategory(category)
// Found the item
return &p, true, nil
}

├───internal
│   ├───product
│   │   ├───infrastructure
│   │   │       productRepository.go

3 - Create port.go (dp injection for the service)

package product
// Product port interface definition for depedency injection
type ProductRepository interface {
Save(p *Product) (int64, error)
GetBySku(sku string) (*Product, bool, error)
}

4 - Create service/createProduct.go (create a new product use case implementation)

├───internal
│   ├───product
│   │   └───service
│   │           createProduct.go
│   │           service.go

package service
import (
"log"
"github.com/sebajax/go-vertical-slice-architecture/internal/product"
"github.com/sebajax/go-vertical-slice-architecture/pkg/apperror"
)
// Product use cases (port injection)
type CreateProductService struct {
productRepository product.ProductRepository
}
// Create a new product service use case instance
func NewCreateProductService(repository product.ProductRepository) *CreateProductService {
// return the pointer to product service
return &CreateProductService{
productRepository: repository,
}
}
// Create a new product and store the product into the database
func (service *CreateProductService) CreateProduct(p *product.Product) (int64, error) {
_, check, err := service.productRepository.GetBySku(p.Sku)
// check if product sky does not exist and no database error ocurred
if err != nil {
// database error
log.Fatalln(err)
err := apperror.InternalServerError()
return 0, err
}
if check {
// product sku found
log.Println(p, product.ErrorSkuExists)
err := apperror.BadRequest(product.ErrorSkuExists)
return 0, err
}
// create the new product and return the id
id, err := service.productRepository.Save(p)
if err != nil {
// database error
log.Fatalln(err)
err := apperror.InternalServerError()
return 0, err
}
// product created successfuly
return id, nil
}

5 - Create service/service.go (dependency injection service using uber dig)

├───internal
│   ├───product
│   │   └───service
│   │           createProduct.go
│   │           service.go

package service
import (
"github.com/sebajax/go-vertical-slice-architecture/internal/product/infrastructure"
"go.uber.org/dig"
)
// product service instance
type ProductService struct {
CreateProductServiceProvider *CreateProductService
}
func NewProductService() *ProductService {
return &ProductService{}
}
// provide components for injection
func ProvideProductComponents(c *dig.Container) {
// repositorory provider injection
err := c.Provide(infrastructure.NewProductRepository)
if err != nil {
panic(err)
}
// service provider injection
err = c.Provide(NewCreateProductService)
if err != nil {
panic(err)
}
}
// init service container
func (ps *ProductService) InitProductComponents(c *dig.Container) error {
// create product service
err := c.Invoke(
func(s *CreateProductService) {
ps.CreateProductServiceProvider = s
},
)
return err
}

6 - Create handler/createProduct.go (create a new hanlder presenter to call the use case)

├───internal
│   ├───product
│   │   ├───handler
│   │   │       createProduct.go
│   │   │       handler.go

package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/sebajax/go-vertical-slice-architecture/internal/product"
"github.com/sebajax/go-vertical-slice-architecture/internal/product/service"
"github.com/sebajax/go-vertical-slice-architecture/pkg/apperror"
"github.com/sebajax/go-vertical-slice-architecture/pkg/message"
"github.com/sebajax/go-vertical-slice-architecture/pkg/validate"
)
// Body request schema for CreateProduct
type ProductSchema struct {
Name string `json:"name" validate:"required,min=5"`
Sku string `json:"sku" validate:"required,min=8"`
Category string `json:"category" validate:"required,min=5"`
Price float64 `json:"price" validate:"required"`
}
// Creates a new product into the database
func CreateProduct(s *service.CreateProductService) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get body request
var body ProductSchema
// Validate the body
err := c.BodyParser(&body)
if err != nil {
// Map the error and response via the middleware
log.Error(err)
return err
}
// Validate schema
serr, err := validate.Validate(body)
if err != nil {
log.Error(serr)
return apperror.BadRequest(serr)
}
// No schema errores then map body to domain
p := &product.Product{
Name: body.Name,
Sku: body.Sku,
Category: product.ParseProductCategory(body.Category),
Price: body.Price,
}
// Execute the service
result, err := s.CreateProduct(p)
if err != nil {
// if service response an error return via the middleware
log.Error(err)
return err
}
// Success execution
return c.Status(fiber.StatusCreated).JSON(message.SuccessResponse(&result))
}
}

7 - Create handler/handler.go (handles all the routes for all the presenters)

├───internal
│   ├───product
│   │   ├───handler
│   │   │       createProduct.go
│   │   │       handler.go

package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/sebajax/go-vertical-slice-architecture/internal/product/service"
)
// ProductRouter is the Router for GoFiber App
func ProductRouter(app fiber.Router, s *service.ProductService) {
app.Post("/", CreateProduct(s.CreateProductServiceProvider))
}

8 - Create mock/ProductRepository.go (mocks the user repository implementation for unit testing)

├───internal
│   ├───product
│   │   ├───mock
│   │   │       mockProductRepository.go
    # It uses mockery Run
        mockery

9 - Create service/createProduct_test.go (create a unit test for the service)

├───internal
│   ├───product
│   │   └───service
│   │           createProduct.go
│   │           service.go

package service
import (
"errors"
"testing"
"github.com/sebajax/go-vertical-slice-architecture/internal/product"
"github.com/sebajax/go-vertical-slice-architecture/internal/product/mock"
"github.com/stretchr/testify/assert"
)
func TestCreateProductService_CreateProduct_Success(t *testing.T) {
var insertedId int64 = 1
// Create a new Product
p, _ := product.New("test_product", "test_sku", product.Laptop, 1200)
// Create a mock ProductRepository instance
mockProductRepository := &mock.ProductRepository{}
// Set expectations on the mock
mockProductRepository.EXPECT().GetBySku(p.Sku).Return(nil, false, nil)
mockProductRepository.EXPECT().Save(p).Return(insertedId, nil)
// Pass the mock as a dependency
productService := NewCreateProductService(mockProductRepository)
// Call the method being tested
id, err := productService.CreateProduct(p)
// Assert the result
assert.NoError(t, err)
assert.Equal(t, insertedId, id)
// Assert that the method was called exactly once or not called
mockProductRepository.AssertCalled(t, "GetBySku", p.Sku)
mockProductRepository.AssertCalled(t, "Save", p)
// Assert that the expectations were met
mockProductRepository.AssertExpectations(t)
}
func TestCreateProductService_CreateProduct_GetBySku_Failure(t *testing.T) {
// Create a new Product
p, _ := product.New("test_product", "test_sku", product.Laptop, 1200)
// Create a mock ProductRepository instance
mockProductRepository := &mock.ProductRepository{}
// Set expectations on the mock
mockProductRepository.EXPECT().GetBySku(p.Sku).Return(p, true, nil)
// Pass the mock as a dependency
productService := NewCreateProductService(mockProductRepository)
// Call the method being tested
id, err := productService.CreateProduct(p)
// Assert the result
assert.Error(t, err)
assert.Equal(t, int64(0), id)
// Assert that the method was called exactly once or not called
mockProductRepository.AssertCalled(t, "GetBySku", p.Sku)
mockProductRepository.AssertNotCalled(t, "Save")
// Assert that the expectations were met
mockProductRepository.AssertExpectations(t)
}
func TestCreateProductService_CreateProduct_Save_Failure(t *testing.T) {
// Create a new Product
p, _ := product.New("test_product", "test_sku", product.Laptop, 1200)
// Create a mock ProductRepository instance
mockProductRepository := &mock.ProductRepository{}
// Set expectations on the mock
mockProductRepository.EXPECT().GetBySku(p.Sku).Return(nil, false, nil)
mockProductRepository.EXPECT().Save(p).Return(0, errors.New("DB ERROR"))
// Pass the mock as a dependency
productService := NewCreateProductService(mockProductRepository)
// Call the method being tested
id, err := productService.CreateProduct(p)
// Assert the result
assert.Error(t, err)
assert.Equal(t, int64(0), id)
// Assert that the method was called exactly once or not called
mockProductRepository.AssertCalled(t, "GetBySku", p.Sku)
mockProductRepository.AssertCalled(t, "Save", p)
// Assert that the expectations were met
mockProductRepository.AssertExpectations(t)
}

10 - Add dependency injection service using uber dig

├───pkg
│   ├───injection

package injection
import (
"os"
productservice "github.com/sebajax/go-vertical-slice-architecture/internal/product/service"
userservice "github.com/sebajax/go-vertical-slice-architecture/internal/user/service"
"github.com/sebajax/go-vertical-slice-architecture/pkg/database"
"go.uber.org/dig"
)
// container instance
var container *dig.Container
// new user service instace
var UserServiceProvider *userservice.UserService
// new product service instace
var ProductServiceProvider *productservice.ProductService
// provide components for injection
func ProvideComponents() {
// create a new container
container = dig.New()
// generate db config instance
err := container.Provide(func() *database.DbConfig {
config := database.NewDbConfig(
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
)
config.WithMigration(os.Getenv("MIGRATIONS_PATH"))
return config
})
if err != nil {
panic(err)
}
// provide the database connection injection
err = container.Provide(database.InitPool)
if err != nil {
panic(err)
}
// user provider injection
userservice.ProvideUserComponents(container)
// product provider injection
productservice.ProvideProductComponents(container)
}
// init service container
func InitComponents() error {
// user service init componets injection
UserServiceProvider = userservice.NewUserService()
err := UserServiceProvider.InitUserComponents(container)
if err != nil {
panic(err)
}
// product service init componets injection
ProductServiceProvider = productservice.NewProductService()
err = ProductServiceProvider.InitProductComponents(container)
if err != nil {
panic(err)
}
return err
}

⚙️ Usage without Make

Docker usage

    # Build server
        docker-compose -p go-vertical-slice-architecture build

    # Start server
        docker-compose up -d

    # Stop server
        docker-compose down

Standalone usage

    # Live reload
        air

Testing

    # To run unit testing
        go test ./...

    # To run unit testing coverage
        go test -cover ./...

Formatting, Linting and Vetting

    # Clean dependencies
        go mod tidy

    # Run formating
        go fmt ./...

    # Remove unused imports
        goimports -l -w .

    # Run linting
        golangci-lint run ./...

    # Run vetting
        go vet ./...

    # Run shadow to check shadowed variables
        # Install shadow
        go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
        # Run shadow
        shadow ./...

Database migration script

    # Create the script
        migrate create -ext sql -dir /migrations -seq [script_name]
    # Run the script
        migrate -database ${POSTGRESQL_URL} -path /migrations up

    # It will run automatically when the database initializes

⚙️ Usage with Make

Docker usage

    # Build server
        make build-server

    # Start server
        make start-server

    # Stop server
        make stop-server

Standalone usage

    # Live reload
        make live-reload

Testing

    # To run unit testing
        make test

    # To run unit testing coverage
        make test-coverage

Formatting, Linting and Vetting

    # Clean dependencies
        make clean-deps

    # Run formating
        make format

    # Remove unused imports
        make clean-imports

    # Run linting
        make lint

    # Run vetting
        make vet

    # Run shadow to check shadowed variables
        # Install shadow
        go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
        # Run shadow
        make check-shadow

    # Run vetting to lint, format and vet your once
        make lint-format

Database migration script

    # Create the script (replace your_script_name with the actual name)
        make migrate-create name=your_script_name
    # Run the script
        make migrate-up

    # It will run automatically when the database initializes

💻 Environment variables

To modify/add configuration via environment variables, use the .env file, which contains basic app configuration.

About

go-vertical-slice-architecture

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published