Penerapan Domain-Driven Design dan CQRS Pattern di Golang untuk Pemula
Yoga Meleniawan Pamungkas
Posted on June 7, 2024
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?
- 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.
- Komunikasi Lebih Baik: DDD menggunakan bahasa yang sama dengan domain bisnis (Ubiquitous Language), sehingga komunikasi antara tim teknis dan stakeholder bisnis menjadi lebih efektif.
- Struktur Kode yang Jelas: Dengan DDD, kode kita terstruktur berdasarkan domain dan subdomain. Ini membuat kode lebih mudah dipahami dan di-maintain.
- 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?
- 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.
- 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.
- 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.
- 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
Entity (Product)
// internal/domain/product/product.go
package product
type Product struct {
ID string
Name string
Price float64
}
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
}
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
}
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)
}
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
}
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)
}
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))
}
Penjelasan
-
Entity: Define
Product
dengan atributID
,Name
, danPrice
. - Infrastructure: Contoh koneksi ke SQLite database.
-
Commands:
CreateProductCommand
danCreateProductHandler
untuk handle operasi tulis. -
Queries:
GetProductQuery
danGetProductHandler
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!
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
November 29, 2024