Penerapan Domain-Driven Design dan CQRS Pattern di Golang untuk Pemula
Tags: go
Published: 2024-06-07 02:27:03


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2f0xdlf4mjtn7uk8omn.png)

### 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](https://yogameleniawan.com/learning-media/memahami-cqrs-command-query-responsibility-segregation-kenapa-dan-bagaimana-menggunakannya-4hmf)

### Contoh Implementasi DDD dan CQRS di Golang

**Struktur Folder dan File**

```bash
/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

```

**Entity (Product)**

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

type Product struct {
    ID    string
    Name  string
    Price float64
}

```

**Infrastructure (Database)**

```go
// 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
}

```

**Commands (CreateProduct)**

```go
// 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
}

```

**Queries (GetProduct)**

```go
// 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)
}

```

**Repository (Data Access)**

```go
// 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
}

```

**Interface (HTTP Handler)**

```go
// 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)
}

```

**Entry Point (main.go)**

```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))
}

```

**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!