go

Penerapan Domain-Driven Design dan CQRS Pattern di Golang untuk Pemula

yogameleniawan

Yoga Meleniawan Pamungkas

Posted on June 7, 2024

Penerapan Domain-Driven Design dan CQRS Pattern di Golang untuk Pemula

Image description

Apa Itu DDD (Domain-Driven Design) Arsitektur?

Halo temen-temen! Jadi, Domain-Driven Design (DDD) itu sebuah pendekatan dalam pengembangan perangkat lunak yang fokus utamanya adalah pada domain bisnis yang dihadapi oleh aplikasi tersebut. Dalam DDD, kita lebih memperhatikan logika bisnis dan bagaimana cara memodelkan domain tersebut dengan cara yang mudah dipahami oleh tim pengembang dan stakeholder bisnis.

Kenapa Harus Menggunakan DDD?

  1. Fokus pada Domain: DDD membantu kita fokus pada domain bisnis dan logika yang terkait dengan domain tersebut. Ini berarti kita memodelkan kode berdasarkan bagaimana bisnis berjalan.
  2. Komunikasi Lebih Baik: DDD menggunakan bahasa yang sama dengan domain bisnis (Ubiquitous Language), sehingga komunikasi antara tim teknis dan stakeholder bisnis menjadi lebih efektif.
  3. Struktur Kode yang Jelas: Dengan DDD, kode kita terstruktur berdasarkan domain dan subdomain. Ini membuat kode lebih mudah dipahami dan di-maintain.
  4. Skalabilitas: DDD mendukung modularitas dan skalabilitas. Ketika bisnis berkembang, aplikasi juga bisa berkembang tanpa perlu perubahan besar-besaran.

Apa Itu CQRS (Command Query Responsibility Segregation) Pattern?

CQRS adalah pola desain yang memisahkan operasi baca (query) dan tulis (command) dalam aplikasi. Jadi, daripada menggabungkan operasi baca dan tulis dalam satu model, kita memisahkannya menjadi dua model yang berbeda. Hal ini memungkinkan kita untuk mengoptimalkan dan menskalakan masing-masing operasi secara independen.

Kenapa Pakai CQRS Pattern?

  1. Optimalisasi Kinerja: Dengan memisahkan operasi read dan write, kita bisa mengoptimalkan masing-masing operasi sesuai kebutuhan. Misalnya, kita bisa menggunakan caching untuk operasi read tanpa mempengaruhi operasi write.
  2. Skalabilitas: CQRS memungkinkan aplikasi untuk diskalakan secara independen antara bagian yang menangani query dan command. Ini sangat berguna ketika aplikasi mulai tumbuh besar dan beban kerja meningkat.
  3. Simplifikasi Model Data: Memisahkan model data untuk read dan write dapat menyederhanakan desain database. Model query bisa dioptimalkan untuk performa read, sementara model command bisa dioptimalkan untuk read dan transaction.
  4. Isolasi Logika Bisnis: CQRS membantu dalam memisahkan logika bisnis dari logika presentasi dan data, sehingga membuat kode lebih bersih dan mudah di-maintain.

Untuk artikel lengkapnya temen-temen bisa buka artikel tentang CQRS disini ya

Contoh Implementasi DDD dan CQRS di Golang

Struktur Folder dan File

/project
  /cmd
    /app
      main.go
  /internal
    /domain
      /product
        product.go
    /infrastructure
      /db
        db.go
      /http
        http.go
    /application
      /product
        /commands
          create_product.go
        /queries
          get_product.go
    /interfaces
      /http
        product_handler.go

Enter fullscreen mode Exit fullscreen mode

Entity (Product)

// internal/domain/product/product.go
package product

type Product struct {
    ID    string
    Name  string
    Price float64
}

Enter fullscreen mode Exit fullscreen mode

Infrastructure (Database)

// internal/infrastructure/db/db.go
package db

import (
    "database/sql"
    "fmt"

    _ "github.com/mattn/go-sqlite3"
)

func NewSQLiteDB(dataSourceName string) (*sql.DB, error) {
    db, err := sql.Open("sqlite3", dataSourceName)
    if err != nil {
        return nil, err
    }

    if err := db.Ping(); err != nil {
        return nil, err
    }

    fmt.Println("Connected to the database successfully!")
    return db, nil
}

Enter fullscreen mode Exit fullscreen mode

Commands (CreateProduct)

// internal/application/product/commands/create_product.go
package commands

import (
    "context"

    "github.com/yogameleniawan/ddd_project/internal/domain/product"
)

type CreateProductCommand struct {
    Name  string
    Price float64
}

type CreateProductHandler struct {
    repo product.Repository
}

func NewCreateProductHandler(repo product.Repository) *CreateProductHandler {
    return &CreateProductHandler{repo}
}

func (h *CreateProductHandler) Handle(ctx context.Context, cmd CreateProductCommand) (product.Product, error) {
    p := product.Product{
        ID:    generateID(), // Bisa pakai UUID
        Name:  cmd.Name,
        Price: cmd.Price,
    }
    err := h.repo.Save(p)
    if err != nil {
        return product.Product{}, err
    }
    return p, nil
}

Enter fullscreen mode Exit fullscreen mode

Queries (GetProduct)

// internal/application/product/queries/get_product.go
package queries

import (
    "context"

    "github.com/yogameleniawan/ddd_project/internal/domain/product"
)

type GetProductQuery struct {
    ID string
}

type GetProductHandler struct {
    repo product.Repository
}

func NewGetProductHandler(repo product.Repository) *GetProductHandler {
    return &GetProductHandler{repo}
}

func (h *GetProductHandler) Handle(ctx context.Context, query GetProductQuery) (product.Product, error) {
    return h.repo.FindByID(query.ID)
}

Enter fullscreen mode Exit fullscreen mode

Repository (Data Access)

// internal/domain/product/repository.go
package product

type Repository interface {
    Save(product Product) error
    FindByID(id string) (Product, error)
}

type sqliteRepository struct {
    db *sql.DB
}

func NewSQLiteRepository(db *sql.DB) Repository {
    return &sqliteRepository{db}
}

func (r *sqliteRepository) Save(product Product) error {
    _, err := r.db.Exec("INSERT INTO products (id, name, price) VALUES (?, ?, ?)", product.ID, product.Name, product.Price)
    return err
}

func (r *sqliteRepository) FindByID(id string) (Product, error) {
    row := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id)
    var product Product
    err := row.Scan(&product.ID, &product.Name, &product.Price)
    if err != nil {
        return Product{}, err
    }
    return product, nil
}

Enter fullscreen mode Exit fullscreen mode

Interface (HTTP Handler)

// internal/interfaces/http/product_handler.go
package http

import (
    "encoding/json"
    "net/http"

    "github.com/yogameleniawan/ddd_project/internal/application/product/commands"
    "github.com/yogameleniawan/ddd_project/internal/application/product/queries"
)

type ProductHandler struct {
    createHandler *commands.CreateProductHandler
    getHandler    *queries.GetProductHandler
}

func NewProductHandler(createHandler *commands.CreateProductHandler, getHandler *queries.GetProductHandler) *ProductHandler {
    return &ProductHandler{createHandler, getHandler}
}

func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Name  string  `json:"name"`
        Price float64 `json:"price"`
    }

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    cmd := commands.CreateProductCommand{
        Name:  req.Name,
        Price: req.Price,
    }

    product, err := h.createHandler.Handle(r.Context(), cmd)
    if (err != nil) {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(product)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")

    query := queries.GetProductQuery{
        ID: id,
    }

    product, err := h.getHandler.Handle(r.Context(), query)
    if (err != nil) {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(product)
}

Enter fullscreen mode Exit fullscreen mode

Entry Point (main.go)

// cmd/app/main.go
package main

import (
    "log"
    "net/http"

    "github.com/yogameleniawan/ddd_project/internal/infrastructure/db"
    "github.com/yogameleniawan/ddd_project/internal/domain/product"
    "github.com/yogameleniawan/ddd_project/internal/application/product/commands"
    "github.com/yogameleniawan/ddd_project/internal/application/product/queries"
    "github.com/yogameleniawan/ddd_project/internal/interfaces/http"
)

func main() {
    dbConn, err := db.NewSQLiteDB("file:products.db?cache=shared&mode=memory")
    if err != nil {
        log.Fatalf("could not connect to the database: %v", err)
    }

    productRepo := product.NewSQLiteRepository(dbConn)
    createProductHandler := commands.NewCreateProductHandler(productRepo)
    getProductHandler := queries.NewGetProductHandler(productRepo)
    productHandler := http.NewProductHandler(createProductHandler, getProductHandler)

    http.HandleFunc("/products", productHandler.CreateProduct)
    http.HandleFunc("/product", productHandler.GetProduct)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Enter fullscreen mode Exit fullscreen mode

Penjelasan

  • Entity: Define Product dengan atribut ID, Name, dan Price.
  • Infrastructure: Contoh koneksi ke SQLite database.
  • Commands: CreateProductCommand dan CreateProductHandler untuk handle operasi tulis.
  • Queries: GetProductQuery dan GetProductHandler untuk handle operasi read.
  • Repository: Implementasi repository untuk data access.
  • HTTP Handler: Handler untuk HTTP request yang meng-handle operasi read dan write.
  • Main: Entry point aplikasi, menyambungkan semua komponen dan menjalankan HTTP server.

Dengan CQRS, kita bisa misahin logika baca dan tulis sehingga performa aplikasi lebih optimal dan kode lebih terstruktur. Semoga penjelasan ini membantu, bro! Happy coding! Jangan lupa ngoding itu diketik jangan dipikir, sampai bertemu di artikel lainnya bro!

đź’– đź’Ş đź™… đźš©
yogameleniawan
Yoga Meleniawan Pamungkas

Posted on June 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related