NATS & JetStream in Go: Cloud-Native Messaging at Scale

    Backend Communication Current: NATS & JetStream WebRTC All Posts Apache Kafka What is NATS? NATS is a high-performance, cloud-native messaging system designed for microservices, IoT, and edge computing. It provides a simple yet powerful pub/sub model with subject-based addressing, making it ideal for building distributed systems that require fast, reliable communication. ...

    February 7, 2025 · 12 min · Rafiul Alam

    WebRTC in Go: Peer-to-Peer Real-Time Communication

    Backend Communication Current: WebRTC gRPC Streaming All Posts NATS & JetStream What is WebRTC? WebRTC (Web Real-Time Communication) enables peer-to-peer audio, video, and data sharing directly between browsers and native applications. Unlike traditional client-server models, WebRTC allows clients to communicate directly with each other after establishing a connection through a signaling server. ...

    February 4, 2025 · 14 min · Rafiul Alam

    gRPC Streaming in Go: High-Performance Inter-Service Communication

    Backend Communication Current: gRPC Streaming WebSockets All Posts WebRTC What is gRPC Streaming? gRPC (gRPC Remote Procedure Call) is a high-performance, open-source RPC framework that uses HTTP/2 for transport, Protocol Buffers for serialization, and provides built-in support for streaming. Unlike traditional request-response RPCs, gRPC streaming enables long-lived connections where either party can send multiple messages over time. ...

    February 1, 2025 · 15 min · Rafiul Alam

    WebSockets in Go: Building Real-Time Bidirectional Communication

    Backend Communication Current: WebSockets Server-Sent Events All Posts gRPC Streaming What are WebSockets? WebSockets provide full-duplex, bidirectional communication channels over a single TCP connection. Unlike HTTP’s request-response model, WebSockets enable both client and server to send messages independently at any time, making them ideal for real-time applications. ...

    January 29, 2025 · 18 min · Rafiul Alam

    Server-Sent Events (SSE) in Go: Real-Time Server-to-Client Streaming

    Backend Communication Current: Server-Sent Events HTTP Polling Patterns All Posts WebSockets What are Server-Sent Events (SSE)? Server-Sent Events (SSE) is a server push technology that enables servers to push real-time updates to clients over a single HTTP connection. Unlike WebSockets, SSE is uni-directional (server → client only) and uses the standard HTTP protocol, making it simpler and more reliable through proxies and firewalls. ...

    January 26, 2025 · 16 min · Rafiul Alam

    Microservices Architecture in Go: Building Distributed Systems

    Go Architecture Patterns Series: ← Previous: Modular Monolith | Series Overview | Next: Event-Driven Architecture → What is Microservices Architecture? Microservices Architecture is an approach where an application is composed of small, independent services that communicate over a network. Each service is self-contained, owns its data, and can be deployed, scaled, and updated independently. Key Principles: Service Independence: Each service is deployed and scaled independently Single Responsibility: Each service handles a specific business capability Decentralized Data: Each service owns its database API-First Design: Services communicate through well-defined APIs Resilience: Services handle failures gracefully Technology Diversity: Services can use different technologies Architecture Overview %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph TB subgraph "Client Layer" Client[Web/Mobile Client] end subgraph "API Gateway" Gateway[API Gateway / Load Balancer] end subgraph "Service Mesh" UserService[User Service] ProductService[Product Service] OrderService[Order Service] PaymentService[Payment Service] NotificationService[Notification Service] end subgraph "Data Layer" UserDB[(User DB)] ProductDB[(Product DB)] OrderDB[(Order DB)] PaymentDB[(Payment DB)] end subgraph "Infrastructure" MessageBroker[Message Broker] ServiceRegistry[Service Discovery] ConfigServer[Config Server] end Client --> Gateway Gateway --> UserService Gateway --> ProductService Gateway --> OrderService Gateway --> PaymentService UserService --> UserDB ProductService --> ProductDB OrderService --> OrderDB PaymentService --> PaymentDB OrderService -.->|HTTP/gRPC| UserService OrderService -.->|HTTP/gRPC| ProductService OrderService -.->|HTTP/gRPC| PaymentService OrderService -.->|Async| MessageBroker PaymentService -.->|Async| MessageBroker NotificationService -.->|Subscribe| MessageBroker UserService -.-> ServiceRegistry ProductService -.-> ServiceRegistry OrderService -.-> ServiceRegistry style UserService fill:#1e3a5f,color:#fff style ProductService fill:#78350f,color:#fff style OrderService fill:#134e4a,color:#fff style PaymentService fill:#4c1d95,color:#fff style NotificationService fill:#4a1e3a,color:#fff Service Communication Patterns %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% sequenceDiagram participant Client participant Gateway participant OrderSvc as Order Service participant UserSvc as User Service participant ProductSvc as Product Service participant PaymentSvc as Payment Service participant Queue as Message Queue participant NotifySvc as Notification Service Client->>Gateway: Create Order Request Gateway->>OrderSvc: POST /orders OrderSvc->>UserSvc: GET /users/{id} UserSvc-->>OrderSvc: User Data OrderSvc->>ProductSvc: GET /products/{id} ProductSvc-->>OrderSvc: Product Data OrderSvc->>ProductSvc: POST /products/reserve ProductSvc-->>OrderSvc: Stock Reserved OrderSvc->>PaymentSvc: POST /payments PaymentSvc-->>OrderSvc: Payment Success OrderSvc->>Queue: Publish OrderCreated Event Queue->>NotifySvc: OrderCreated Event NotifySvc->>NotifySvc: Send Email/SMS OrderSvc-->>Gateway: Order Created Gateway-->>Client: Response Real-World Use Cases E-commerce Platforms: Amazon, eBay with separate services for products, orders, payments Streaming Services: Netflix with services for recommendations, playback, billing Ride-Sharing Apps: Uber with services for riders, drivers, payments, routing Financial Systems: Banking apps with separate services for accounts, transactions, loans Social Media: Twitter with services for posts, timelines, notifications, messages Cloud Platforms: AWS-like platforms with independent service offerings Microservices Implementation Project Structure (Multi-Repository) microservices/ ├── user-service/ │ ├── cmd/ │ │ └── server/ │ │ └── main.go │ ├── internal/ │ │ ├── domain/ │ │ ├── handlers/ │ │ ├── repository/ │ │ └── service/ │ ├── proto/ │ │ └── user.proto │ └── go.mod ├── product-service/ │ ├── cmd/ │ │ └── server/ │ │ └── main.go │ ├── internal/ │ │ ├── domain/ │ │ ├── handlers/ │ │ ├── repository/ │ │ └── service/ │ └── go.mod ├── order-service/ │ ├── cmd/ │ │ └── server/ │ │ └── main.go │ ├── internal/ │ │ ├── domain/ │ │ ├── handlers/ │ │ ├── repository/ │ │ ├── service/ │ │ └── clients/ │ └── go.mod └── api-gateway/ ├── cmd/ │ └── server/ │ └── main.go └── go.mod Service 1: User Service // user-service/internal/domain/user.go package domain import ( "context" "errors" "time" ) type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Active bool `json:"active"` CreatedAt time.Time `json:"created_at"` } var ( ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") ) type Repository interface { Create(ctx context.Context, user *User) error GetByID(ctx context.Context, id string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) Update(ctx context.Context, user *User) error } // user-service/internal/service/user_service.go package service import ( "context" "fmt" "time" "user-service/internal/domain" ) type UserService struct { repo domain.Repository } func NewUserService(repo domain.Repository) *UserService { return &UserService{repo: repo} } func (s *UserService) CreateUser(ctx context.Context, email, name string) (*domain.User, error) { existing, _ := s.repo.GetByEmail(ctx, email) if existing != nil { return nil, domain.ErrUserExists } user := &domain.User{ ID: generateID(), Email: email, Name: name, Active: true, CreatedAt: time.Now(), } if err := s.repo.Create(ctx, user); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } return user, nil } func (s *UserService) GetUser(ctx context.Context, id string) (*domain.User, error) { return s.repo.GetByID(ctx, id) } func (s *UserService) ValidateUser(ctx context.Context, id string) (bool, error) { user, err := s.repo.GetByID(ctx, id) if err != nil { return false, err } return user.Active, nil } func generateID() string { return fmt.Sprintf("user_%d", time.Now().UnixNano()) } // user-service/internal/handlers/http_handler.go package handlers import ( "encoding/json" "net/http" "github.com/gorilla/mux" "user-service/internal/service" ) type HTTPHandler struct { service *service.UserService } func NewHTTPHandler(service *service.UserService) *HTTPHandler { return &HTTPHandler{service: service} } type CreateUserRequest struct { Email string `json:"email"` Name string `json:"name"` } func (h *HTTPHandler) CreateUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "invalid request") return } user, err := h.service.CreateUser(r.Context(), req.Email, req.Name) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusCreated, user) } func (h *HTTPHandler) GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] user, err := h.service.GetUser(r.Context(), id) if err != nil { respondError(w, http.StatusNotFound, "user not found") return } respondJSON(w, http.StatusOK, user) } func (h *HTTPHandler) ValidateUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] valid, err := h.service.ValidateUser(r.Context(), id) if err != nil { respondError(w, http.StatusNotFound, "user not found") return } respondJSON(w, http.StatusOK, map[string]bool{"valid": valid}) } func respondJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func respondError(w http.ResponseWriter, status int, message string) { respondJSON(w, status, map[string]string{"error": message}) } // user-service/cmd/server/main.go package main import ( "database/sql" "log" "net/http" "os" "github.com/gorilla/mux" _ "github.com/lib/pq" "user-service/internal/handlers" "user-service/internal/repository" "user-service/internal/service" ) func main() { dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { dbURL = "postgres://user:pass@localhost/users?sslmode=disable" } db, err := sql.Open("postgres", dbURL) if err != nil { log.Fatal(err) } defer db.Close() repo := repository.NewPostgresRepository(db) svc := service.NewUserService(repo) handler := handlers.NewHTTPHandler(svc) router := mux.NewRouter() router.HandleFunc("/users", handler.CreateUser).Methods("POST") router.HandleFunc("/users/{id}", handler.GetUser).Methods("GET") router.HandleFunc("/users/{id}/validate", handler.ValidateUser).Methods("GET") router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) port := os.Getenv("PORT") if port == "" { port = "8081" } log.Printf("User service starting on port %s", port) if err := http.ListenAndServe(":"+port, router); err != nil { log.Fatal(err) } } Service 2: Product Service // product-service/internal/domain/product.go package domain import ( "context" "errors" "time" ) type Product struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` Stock int `json:"stock"` CreatedAt time.Time `json:"created_at"` } var ( ErrProductNotFound = errors.New("product not found") ErrInsufficientStock = errors.New("insufficient stock") ) type Repository interface { Create(ctx context.Context, product *Product) error GetByID(ctx context.Context, id string) (*Product, error) Update(ctx context.Context, product *Product) error ReserveStock(ctx context.Context, id string, quantity int) error } // product-service/internal/service/product_service.go package service import ( "context" "fmt" "time" "product-service/internal/domain" ) type ProductService struct { repo domain.Repository } func NewProductService(repo domain.Repository) *ProductService { return &ProductService{repo: repo} } func (s *ProductService) CreateProduct(ctx context.Context, name, desc string, price float64, stock int) (*domain.Product, error) { product := &domain.Product{ ID: generateID(), Name: name, Description: desc, Price: price, Stock: stock, CreatedAt: time.Now(), } if err := s.repo.Create(ctx, product); err != nil { return nil, fmt.Errorf("failed to create product: %w", err) } return product, nil } func (s *ProductService) GetProduct(ctx context.Context, id string) (*domain.Product, error) { return s.repo.GetByID(ctx, id) } func (s *ProductService) ReserveStock(ctx context.Context, id string, quantity int) error { product, err := s.repo.GetByID(ctx, id) if err != nil { return err } if product.Stock < quantity { return domain.ErrInsufficientStock } return s.repo.ReserveStock(ctx, id, quantity) } func generateID() string { return fmt.Sprintf("product_%d", time.Now().UnixNano()) } // product-service/cmd/server/main.go package main import ( "database/sql" "encoding/json" "log" "net/http" "os" "github.com/gorilla/mux" _ "github.com/lib/pq" "product-service/internal/repository" "product-service/internal/service" ) func main() { dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { dbURL = "postgres://user:pass@localhost/products?sslmode=disable" } db, err := sql.Open("postgres", dbURL) if err != nil { log.Fatal(err) } defer db.Close() repo := repository.NewPostgresRepository(db) svc := service.NewProductService(repo) router := mux.NewRouter() router.HandleFunc("/products/{id}", func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) product, err := svc.GetProduct(r.Context(), vars["id"]) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } json.NewEncoder(w).Encode(product) }).Methods("GET") router.HandleFunc("/products/reserve", func(w http.ResponseWriter, r *http.Request) { var req struct { ProductID string `json:"product_id"` Quantity int `json:"quantity"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := svc.ReserveStock(r.Context(), req.ProductID, req.Quantity); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "reserved"}) }).Methods("POST") router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) port := os.Getenv("PORT") if port == "" { port = "8082" } log.Printf("Product service starting on port %s", port) if err := http.ListenAndServe(":"+port, router); err != nil { log.Fatal(err) } } Service 3: Order Service (Orchestrator) // order-service/internal/clients/user_client.go package clients import ( "context" "encoding/json" "fmt" "net/http" "time" ) type UserClient struct { baseURL string httpClient *http.Client } func NewUserClient(baseURL string) *UserClient { return &UserClient{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 5 * time.Second, }, } } func (c *UserClient) ValidateUser(ctx context.Context, userID string) (bool, error) { url := fmt.Sprintf("%s/users/%s/validate", c.baseURL, userID) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return false, err } resp, err := c.httpClient.Do(req) if err != nil { return false, fmt.Errorf("failed to call user service: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return false, fmt.Errorf("user service returned status %d", resp.StatusCode) } var result struct { Valid bool `json:"valid"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return false, err } return result.Valid, nil } // order-service/internal/clients/product_client.go package clients import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) type Product struct { ID string `json:"id"` Name string `json:"name"` Price float64 `json:"price"` Stock int `json:"stock"` } type ProductClient struct { baseURL string httpClient *http.Client } func NewProductClient(baseURL string) *ProductClient { return &ProductClient{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 5 * time.Second, }, } } func (c *ProductClient) GetProduct(ctx context.Context, productID string) (*Product, error) { url := fmt.Sprintf("%s/products/%s", c.baseURL, productID) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to call product service: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("product not found") } var product Product if err := json.NewDecoder(resp.Body).Decode(&product); err != nil { return nil, err } return &product, nil } func (c *ProductClient) ReserveStock(ctx context.Context, productID string, quantity int) error { url := fmt.Sprintf("%s/products/reserve", c.baseURL) reqBody := map[string]interface{}{ "product_id": productID, "quantity": quantity, } body, err := json.Marshal(reqBody) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to reserve stock: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to reserve stock: status %d", resp.StatusCode) } return nil } // order-service/internal/service/order_service.go package service import ( "context" "fmt" "time" "order-service/internal/clients" "order-service/internal/domain" ) type OrderService struct { repo domain.Repository userClient *clients.UserClient productClient *clients.ProductClient } func NewOrderService( repo domain.Repository, userClient *clients.UserClient, productClient *clients.ProductClient, ) *OrderService { return &OrderService{ repo: repo, userClient: userClient, productClient: productClient, } } func (s *OrderService) CreateOrder(ctx context.Context, userID string, items []domain.OrderItem) (*domain.Order, error) { // Validate user via User Service valid, err := s.userClient.ValidateUser(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to validate user: %w", err) } if !valid { return nil, fmt.Errorf("user is not valid") } // Validate products and calculate total var total float64 for i, item := range items { product, err := s.productClient.GetProduct(ctx, item.ProductID) if err != nil { return nil, fmt.Errorf("failed to get product: %w", err) } items[i].Price = product.Price total += product.Price * float64(item.Quantity) } // Reserve stock via Product Service for _, item := range items { if err := s.productClient.ReserveStock(ctx, item.ProductID, item.Quantity); err != nil { return nil, fmt.Errorf("failed to reserve stock: %w", err) } } order := &domain.Order{ ID: generateID(), UserID: userID, Items: items, Total: total, Status: "pending", CreatedAt: time.Now(), } if err := s.repo.Create(ctx, order); err != nil { return nil, fmt.Errorf("failed to create order: %w", err) } return order, nil } func generateID() string { return fmt.Sprintf("order_%d", time.Now().UnixNano()) } // order-service/cmd/server/main.go package main import ( "database/sql" "encoding/json" "log" "net/http" "os" "github.com/gorilla/mux" _ "github.com/lib/pq" "order-service/internal/clients" "order-service/internal/domain" "order-service/internal/repository" "order-service/internal/service" ) func main() { dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { dbURL = "postgres://user:pass@localhost/orders?sslmode=disable" } userServiceURL := os.Getenv("USER_SERVICE_URL") if userServiceURL == "" { userServiceURL = "http://localhost:8081" } productServiceURL := os.Getenv("PRODUCT_SERVICE_URL") if productServiceURL == "" { productServiceURL = "http://localhost:8082" } db, err := sql.Open("postgres", dbURL) if err != nil { log.Fatal(err) } defer db.Close() repo := repository.NewPostgresRepository(db) userClient := clients.NewUserClient(userServiceURL) productClient := clients.NewProductClient(productServiceURL) svc := service.NewOrderService(repo, userClient, productClient) router := mux.NewRouter() router.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) { var req struct { UserID string `json:"user_id"` Items []domain.OrderItem `json:"items"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } order, err := svc.CreateOrder(r.Context(), req.UserID, req.Items) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(order) }).Methods("POST") router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) port := os.Getenv("PORT") if port == "" { port = "8083" } log.Printf("Order service starting on port %s", port) if err := http.ListenAndServe(":"+port, router); err != nil { log.Fatal(err) } } Docker Compose Setup version: '3.8' services: user-service: build: ./user-service ports: - "8081:8081" environment: - DATABASE_URL=postgres://user:pass@user-db:5432/users?sslmode=disable - PORT=8081 depends_on: - user-db product-service: build: ./product-service ports: - "8082:8082" environment: - DATABASE_URL=postgres://user:pass@product-db:5432/products?sslmode=disable - PORT=8082 depends_on: - product-db order-service: build: ./order-service ports: - "8083:8083" environment: - DATABASE_URL=postgres://user:pass@order-db:5432/orders?sslmode=disable - USER_SERVICE_URL=http://user-service:8081 - PRODUCT_SERVICE_URL=http://product-service:8082 - PORT=8083 depends_on: - order-db - user-service - product-service user-db: image: postgres:15 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=users product-db: image: postgres:15 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=products order-db: image: postgres:15 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=orders Best Practices Service Boundaries: Define clear service boundaries based on business capabilities API Contracts: Use API versioning and maintain backward compatibility Service Discovery: Implement service registry for dynamic service location Circuit Breakers: Prevent cascading failures with circuit breaker pattern Distributed Tracing: Implement tracing to debug cross-service calls Health Checks: Provide health endpoints for monitoring Configuration Management: Externalize configuration Security: Implement service-to-service authentication Common Pitfalls Distributed Monolith: Services too tightly coupled, defeating the purpose Chatty Services: Too many synchronous calls between services Shared Database: Multiple services accessing the same database Ignoring Network Failures: Not handling network errors gracefully No Service Versioning: Breaking changes without versioning Data Consistency Issues: Not handling eventual consistency Over-Engineering: Creating too many small services When to Use Microservices Architecture Use When: ...

    January 25, 2025 · 13 min · Rafiul Alam

    HTTP Polling Patterns in Go: From Simple Polling to Server Push

    Backend Communication Current: HTTP Polling Patterns Previous All Posts Server-Sent Events What are HTTP Polling Patterns? HTTP polling patterns are techniques for achieving near-real-time communication between clients and servers using the standard HTTP request-response model. While not truly real-time like WebSockets, these patterns are simpler to implement, easier to debug, and work reliably across all networks and proxies. ...

    January 23, 2025 · 12 min · Rafiul Alam

    Domain-Driven Design in Go: Building Complex Business Systems

    Go Architecture Patterns Series: ← Hexagonal Architecture | Series Overview | Next: Modular Monolith → What is Domain-Driven Design? Domain-Driven Design (DDD) is a software development approach introduced by Eric Evans that focuses on creating software that matches the business domain. It emphasizes collaboration between technical and domain experts using a common language (Ubiquitous Language) and strategic/tactical patterns to handle complex business logic. Key Principles: Ubiquitous Language: Common language shared by developers and domain experts Bounded Contexts: Explicit boundaries where a particular domain model applies Domain Model: Rich model that captures business rules and behavior Strategic Design: High-level patterns for organizing large systems Tactical Design: Building blocks for implementing domain models DDD Strategic Patterns %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph TB subgraph "E-commerce System" subgraph "Ordering Context" OC[Order Aggregate] OL[Order Line Items] OP[Order Payment] end subgraph "Inventory Context" IC[Product Catalog] IS[Stock Management] IW[Warehouse] end subgraph "Shipping Context" SC[Shipment] SD[Delivery] ST[Tracking] end subgraph "Customer Context" CC[Customer Profile] CA[Address] CP[Preferences] end end OC -.->|Anti-Corruption Layer| IC OC -.->|Shared Kernel| CC SC -.->|Published Language| OC style OC fill:#78350f,color:#fff style IC fill:#1e3a5f,color:#fff style SC fill:#134e4a,color:#fff style CC fill:#4c1d95,color:#fff Bounded Context Map %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph LR subgraph "Sales Context" S[Sales DomainCustomer, Order, Product] end subgraph "Support Context" SP[Support DomainTicket, Customer, Issue] end subgraph "Billing Context" B[Billing DomainInvoice, Payment, Customer] end subgraph "Shared Kernel" SK[Customer Identity] end S -.->|Conformist| SK SP -.->|Customer/Supplier| S B -.->|Partnership| S style S fill:#78350f,color:#fff style SP fill:#1e3a5f,color:#fff style B fill:#134e4a,color:#fff style SK fill:#4c1d95,color:#fff DDD Tactical Patterns %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph TD subgraph "Aggregate Root" AR[OrderAggregate Root] E1[Order LineEntity] E2[Payment InfoEntity] VO1[MoneyValue Object] VO2[AddressValue Object] end subgraph "Domain Services" DS[Pricing Service] DS2[Shipping Calculator] end subgraph "Repositories" R[Order Repository] end AR --> E1 AR --> E2 E1 --> VO1 AR --> VO2 AR -.->|uses| DS R -.->|persists| AR style AR fill:#78350f,stroke:#fb923c,stroke-width:3px,color:#fff style VO1 fill:#1e3a5f,color:#fff style DS fill:#134e4a,color:#fff style R fill:#4c1d95,color:#fff Real-World Use Cases E-commerce Platforms: Complex ordering, inventory, and payment systems Financial Systems: Banking, trading, and payment processing Healthcare Systems: Patient records, appointments, and billing Supply Chain Management: Inventory, shipping, and logistics Enterprise Resource Planning: Multi-domain business systems Insurance Systems: Policy management, claims, and underwriting Project Structure ├── cmd/ │ └── api/ │ └── main.go ├── internal/ │ ├── domain/ │ │ ├── order/ # Order Bounded Context │ │ │ ├── aggregate/ │ │ │ │ └── order.go # Order aggregate root │ │ │ ├── entity/ │ │ │ │ └── order_line.go │ │ │ ├── valueobject/ │ │ │ │ ├── money.go │ │ │ │ └── quantity.go │ │ │ ├── service/ │ │ │ │ └── pricing_service.go │ │ │ ├── repository/ │ │ │ │ └── order_repository.go │ │ │ └── event/ │ │ │ └── order_placed.go │ │ ├── customer/ # Customer Bounded Context │ │ │ ├── aggregate/ │ │ │ └── valueobject/ │ │ └── shared/ # Shared Kernel │ │ └── valueobject/ │ ├── application/ # Application Services │ │ ├── order/ │ │ │ └── order_service.go │ │ └── customer/ │ │ └── customer_service.go │ └── infrastructure/ │ ├── persistence/ │ └── messaging/ └── go.mod Building Blocks: Value Objects package valueobject import ( "errors" "fmt" ) // Money represents a monetary value (Value Object) // Value objects are immutable and compared by value, not identity type Money struct { amount int64 // Amount in smallest currency unit (cents) currency string } // NewMoney creates a new Money value object func NewMoney(amount int64, currency string) (Money, error) { if currency == "" { return Money{}, errors.New("currency cannot be empty") } if amount < 0 { return Money{}, errors.New("amount cannot be negative") } return Money{amount: amount, currency: currency}, nil } // Amount returns the amount func (m Money) Amount() int64 { return m.amount } // Currency returns the currency func (m Money) Currency() string { return m.currency } // Add adds two money values func (m Money) Add(other Money) (Money, error) { if m.currency != other.currency { return Money{}, errors.New("cannot add different currencies") } return Money{ amount: m.amount + other.amount, currency: m.currency, }, nil } // Multiply multiplies money by a quantity func (m Money) Multiply(multiplier int) Money { return Money{ amount: m.amount * int64(multiplier), currency: m.currency, } } // IsZero checks if money is zero func (m Money) IsZero() bool { return m.amount == 0 } // Equals checks equality (value objects compare by value) func (m Money) Equals(other Money) bool { return m.amount == other.amount && m.currency == other.currency } // String returns string representation func (m Money) String() string { return fmt.Sprintf("%d %s", m.amount, m.currency) } // Email represents an email address (Value Object) type Email struct { value string } // NewEmail creates a new Email value object func NewEmail(email string) (Email, error) { if !isValidEmail(email) { return Email{}, errors.New("invalid email format") } return Email{value: email}, nil } // String returns the email string func (e Email) String() string { return e.value } // Equals checks equality func (e Email) Equals(other Email) bool { return e.value == other.value } func isValidEmail(email string) bool { // Simplified validation return len(email) > 3 && contains(email, "@") && contains(email, ".") } func contains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } // Address represents a physical address (Value Object) type Address struct { street string city string state string postalCode string country string } // NewAddress creates a new Address value object func NewAddress(street, city, state, postalCode, country string) (Address, error) { if street == "" || city == "" || country == "" { return Address{}, errors.New("street, city, and country are required") } return Address{ street: street, city: city, state: state, postalCode: postalCode, country: country, }, nil } // Street returns the street func (a Address) Street() string { return a.street } // City returns the city func (a Address) City() string { return a.city } // Country returns the country func (a Address) Country() string { return a.country } // Quantity represents a product quantity (Value Object) type Quantity struct { value int } // NewQuantity creates a new Quantity value object func NewQuantity(value int) (Quantity, error) { if value < 0 { return Quantity{}, errors.New("quantity cannot be negative") } return Quantity{value: value}, nil } // Value returns the quantity value func (q Quantity) Value() int { return q.value } // Add adds two quantities func (q Quantity) Add(other Quantity) Quantity { return Quantity{value: q.value + other.value} } // IsZero checks if quantity is zero func (q Quantity) IsZero() bool { return q.value == 0 } Building Blocks: Entities package entity import ( "time" "myapp/internal/domain/order/valueobject" ) // OrderLine is an entity (has identity) // Entities have identity and lifecycle type OrderLine struct { id string productID string product string quantity valueobject.Quantity unitPrice valueobject.Money createdAt time.Time } // NewOrderLine creates a new order line entity func NewOrderLine(id, productID, product string, quantity valueobject.Quantity, unitPrice valueobject.Money) *OrderLine { return &OrderLine{ id: id, productID: productID, product: product, quantity: quantity, unitPrice: unitPrice, createdAt: time.Now(), } } // ID returns the order line ID (identity) func (ol *OrderLine) ID() string { return ol.id } // ProductID returns the product ID func (ol *OrderLine) ProductID() string { return ol.productID } // Quantity returns the quantity func (ol *OrderLine) Quantity() valueobject.Quantity { return ol.quantity } // UnitPrice returns the unit price func (ol *OrderLine) UnitPrice() valueobject.Money { return ol.unitPrice } // TotalPrice calculates the total price for this line func (ol *OrderLine) TotalPrice() valueobject.Money { return ol.unitPrice.Multiply(ol.quantity.Value()) } // UpdateQuantity updates the quantity func (ol *OrderLine) UpdateQuantity(newQuantity valueobject.Quantity) { ol.quantity = newQuantity } // Payment is an entity representing payment information type Payment struct { id string method PaymentMethod amount valueobject.Money transactionID string status PaymentStatus paidAt time.Time } // PaymentMethod represents payment method type PaymentMethod string const ( PaymentMethodCreditCard PaymentMethod = "credit_card" PaymentMethodDebitCard PaymentMethod = "debit_card" PaymentMethodPayPal PaymentMethod = "paypal" ) // PaymentStatus represents payment status type PaymentStatus string const ( PaymentStatusPending PaymentStatus = "pending" PaymentStatusCompleted PaymentStatus = "completed" PaymentStatusFailed PaymentStatus = "failed" ) Building Blocks: Aggregates package aggregate import ( "errors" "time" "myapp/internal/domain/order/entity" "myapp/internal/domain/order/event" "myapp/internal/domain/order/valueobject" ) // Order is an aggregate root // Aggregate roots maintain consistency boundaries and control access to entities type Order struct { id string customerID string orderLines []*entity.OrderLine shippingAddress valueobject.Address billingAddress valueobject.Address payment *entity.Payment status OrderStatus total valueobject.Money createdAt time.Time updatedAt time.Time // Domain events events []event.DomainEvent } // OrderStatus represents the status of an order type OrderStatus string const ( OrderStatusDraft OrderStatus = "draft" OrderStatusPlaced OrderStatus = "placed" OrderStatusConfirmed OrderStatus = "confirmed" OrderStatusShipped OrderStatus = "shipped" OrderStatusDelivered OrderStatus = "delivered" OrderStatusCancelled OrderStatus = "cancelled" ) // NewOrder creates a new order aggregate func NewOrder(id, customerID string, shippingAddress, billingAddress valueobject.Address) (*Order, error) { if id == "" { return nil, errors.New("order ID is required") } if customerID == "" { return nil, errors.New("customer ID is required") } return &Order{ id: id, customerID: customerID, orderLines: make([]*entity.OrderLine, 0), shippingAddress: shippingAddress, billingAddress: billingAddress, status: OrderStatusDraft, createdAt: time.Now(), updatedAt: time.Now(), events: make([]event.DomainEvent, 0), }, nil } // ID returns the order ID func (o *Order) ID() string { return o.id } // CustomerID returns the customer ID func (o *Order) CustomerID() string { return o.customerID } // Status returns the order status func (o *Order) Status() OrderStatus { return o.status } // Total returns the total amount func (o *Order) Total() valueobject.Money { return o.total } // AddOrderLine adds an order line to the order (aggregate invariant) func (o *Order) AddOrderLine(orderLine *entity.OrderLine) error { // Business rule: Cannot add lines to non-draft orders if o.status != OrderStatusDraft { return errors.New("cannot add items to non-draft order") } // Business rule: Check for duplicate products for _, line := range o.orderLines { if line.ProductID() == orderLine.ProductID() { return errors.New("product already exists in order") } } o.orderLines = append(o.orderLines, orderLine) o.recalculateTotal() o.updatedAt = time.Now() return nil } // RemoveOrderLine removes an order line (aggregate invariant) func (o *Order) RemoveOrderLine(orderLineID string) error { // Business rule: Cannot remove lines from non-draft orders if o.status != OrderStatusDraft { return errors.New("cannot remove items from non-draft order") } for i, line := range o.orderLines { if line.ID() == orderLineID { o.orderLines = append(o.orderLines[:i], o.orderLines[i+1:]...) o.recalculateTotal() o.updatedAt = time.Now() return nil } } return errors.New("order line not found") } // PlaceOrder places the order (state transition) func (o *Order) PlaceOrder() error { // Business rule: Can only place draft orders if o.status != OrderStatusDraft { return errors.New("can only place draft orders") } // Business rule: Order must have at least one line if len(o.orderLines) == 0 { return errors.New("order must have at least one item") } // Business rule: Order must have payment if o.payment == nil { return errors.New("order must have payment information") } o.status = OrderStatusPlaced o.updatedAt = time.Now() // Raise domain event o.addEvent(event.NewOrderPlacedEvent(o.id, o.customerID, o.total)) return nil } // ConfirmOrder confirms the order func (o *Order) ConfirmOrder() error { if o.status != OrderStatusPlaced { return errors.New("can only confirm placed orders") } o.status = OrderStatusConfirmed o.updatedAt = time.Now() o.addEvent(event.NewOrderConfirmedEvent(o.id)) return nil } // ShipOrder marks the order as shipped func (o *Order) ShipOrder() error { if o.status != OrderStatusConfirmed { return errors.New("can only ship confirmed orders") } o.status = OrderStatusShipped o.updatedAt = time.Now() o.addEvent(event.NewOrderShippedEvent(o.id, o.shippingAddress)) return nil } // CancelOrder cancels the order func (o *Order) CancelOrder(reason string) error { // Business rule: Cannot cancel shipped or delivered orders if o.status == OrderStatusShipped || o.status == OrderStatusDelivered { return errors.New("cannot cancel shipped or delivered orders") } if o.status == OrderStatusCancelled { return errors.New("order is already cancelled") } o.status = OrderStatusCancelled o.updatedAt = time.Now() o.addEvent(event.NewOrderCancelledEvent(o.id, reason)) return nil } // AddPayment adds payment to the order func (o *Order) AddPayment(payment *entity.Payment) error { if o.payment != nil { return errors.New("payment already exists") } // Business rule: Payment amount must match order total if !payment.Amount.Equals(o.total) { return errors.New("payment amount must match order total") } o.payment = payment o.updatedAt = time.Now() return nil } // recalculateTotal recalculates the order total func (o *Order) recalculateTotal() { if len(o.orderLines) == 0 { o.total = valueobject.Money{} return } total := o.orderLines[0].TotalPrice() for i := 1; i < len(o.orderLines); i++ { var err error total, err = total.Add(o.orderLines[i].TotalPrice()) if err != nil { // Handle error - in production, log this return } } o.total = total } // GetDomainEvents returns all domain events func (o *Order) GetDomainEvents() []event.DomainEvent { return o.events } // ClearDomainEvents clears all domain events func (o *Order) ClearDomainEvents() { o.events = make([]event.DomainEvent, 0) } // addEvent adds a domain event func (o *Order) addEvent(e event.DomainEvent) { o.events = append(o.events, e) } // OrderLines returns a copy of order lines func (o *Order) OrderLines() []*entity.OrderLine { // Return copy to prevent external modification lines := make([]*entity.OrderLine, len(o.orderLines)) copy(lines, o.orderLines) return lines } Building Blocks: Domain Events package event import ( "time" "myapp/internal/domain/order/valueobject" ) // DomainEvent is the base interface for all domain events type DomainEvent interface { OccurredAt() time.Time EventType() string } // OrderPlacedEvent is raised when an order is placed type OrderPlacedEvent struct { orderID string customerID string total valueobject.Money occurredAt time.Time } // NewOrderPlacedEvent creates a new OrderPlacedEvent func NewOrderPlacedEvent(orderID, customerID string, total valueobject.Money) *OrderPlacedEvent { return &OrderPlacedEvent{ orderID: orderID, customerID: customerID, total: total, occurredAt: time.Now(), } } // OrderID returns the order ID func (e *OrderPlacedEvent) OrderID() string { return e.orderID } // CustomerID returns the customer ID func (e *OrderPlacedEvent) CustomerID() string { return e.customerID } // Total returns the total amount func (e *OrderPlacedEvent) Total() valueobject.Money { return e.total } // OccurredAt returns when the event occurred func (e *OrderPlacedEvent) OccurredAt() time.Time { return e.occurredAt } // EventType returns the event type func (e *OrderPlacedEvent) EventType() string { return "OrderPlaced" } // OrderConfirmedEvent is raised when an order is confirmed type OrderConfirmedEvent struct { orderID string occurredAt time.Time } // NewOrderConfirmedEvent creates a new OrderConfirmedEvent func NewOrderConfirmedEvent(orderID string) *OrderConfirmedEvent { return &OrderConfirmedEvent{ orderID: orderID, occurredAt: time.Now(), } } // OrderID returns the order ID func (e *OrderConfirmedEvent) OrderID() string { return e.orderID } // OccurredAt returns when the event occurred func (e *OrderConfirmedEvent) OccurredAt() time.Time { return e.occurredAt } // EventType returns the event type func (e *OrderConfirmedEvent) EventType() string { return "OrderConfirmed" } // OrderShippedEvent is raised when an order is shipped type OrderShippedEvent struct { orderID string shippingAddress valueobject.Address occurredAt time.Time } // NewOrderShippedEvent creates a new OrderShippedEvent func NewOrderShippedEvent(orderID string, shippingAddress valueobject.Address) *OrderShippedEvent { return &OrderShippedEvent{ orderID: orderID, shippingAddress: shippingAddress, occurredAt: time.Now(), } } // EventType returns the event type func (e *OrderShippedEvent) EventType() string { return "OrderShipped" } // OccurredAt returns when the event occurred func (e *OrderShippedEvent) OccurredAt() time.Time { return e.occurredAt } // OrderCancelledEvent is raised when an order is cancelled type OrderCancelledEvent struct { orderID string reason string occurredAt time.Time } // NewOrderCancelledEvent creates a new OrderCancelledEvent func NewOrderCancelledEvent(orderID, reason string) *OrderCancelledEvent { return &OrderCancelledEvent{ orderID: orderID, reason: reason, occurredAt: time.Now(), } } // EventType returns the event type func (e *OrderCancelledEvent) EventType() string { return "OrderCancelled" } // OccurredAt returns when the event occurred func (e *OrderCancelledEvent) OccurredAt() time.Time { return e.occurredAt } Building Blocks: Domain Services package service import ( "errors" "myapp/internal/domain/order/valueobject" ) // PricingService is a domain service for calculating prices // Domain services contain business logic that doesn't belong to any entity type PricingService struct { taxRate float64 } // NewPricingService creates a new pricing service func NewPricingService(taxRate float64) *PricingService { return &PricingService{taxRate: taxRate} } // CalculateOrderTotal calculates the total price with tax and shipping func (s *PricingService) CalculateOrderTotal( subtotal valueobject.Money, shippingCost valueobject.Money, ) (valueobject.Money, error) { // Add shipping to subtotal total, err := subtotal.Add(shippingCost) if err != nil { return valueobject.Money{}, err } // Calculate tax taxAmount := int64(float64(total.Amount()) * s.taxRate) tax, err := valueobject.NewMoney(taxAmount, total.Currency()) if err != nil { return valueobject.Money{}, err } // Add tax to total return total.Add(tax) } // ApplyDiscount applies discount to money func (s *PricingService) ApplyDiscount(amount valueobject.Money, discountPercent int) (valueobject.Money, error) { if discountPercent < 0 || discountPercent > 100 { return valueobject.Money{}, errors.New("invalid discount percentage") } discountAmount := amount.Amount() * int64(discountPercent) / 100 finalAmount := amount.Amount() - discountAmount return valueobject.NewMoney(finalAmount, amount.Currency()) } // ShippingCalculator is a domain service for calculating shipping costs type ShippingCalculator struct{} // NewShippingCalculator creates a new shipping calculator func NewShippingCalculator() *ShippingCalculator { return &ShippingCalculator{} } // CalculateShippingCost calculates shipping cost based on weight and distance func (s *ShippingCalculator) CalculateShippingCost( weightKg float64, distanceKm float64, currency string, ) (valueobject.Money, error) { // Simple formula: base rate + weight factor + distance factor baseRate := int64(500) // $5.00 base weightFactor := int64(weightKg * 100) distanceFactor := int64(distanceKm * 10) totalCost := baseRate + weightFactor + distanceFactor return valueobject.NewMoney(totalCost, currency) } Building Blocks: Repositories package repository import ( "context" "myapp/internal/domain/order/aggregate" ) // OrderRepository defines the repository interface for orders // Repositories provide collection-like interface for aggregates type OrderRepository interface { // Save saves an order aggregate Save(ctx context.Context, order *aggregate.Order) error // FindByID finds an order by ID FindByID(ctx context.Context, id string) (*aggregate.Order, error) // FindByCustomerID finds orders by customer ID FindByCustomerID(ctx context.Context, customerID string) ([]*aggregate.Order, error) // Update updates an existing order Update(ctx context.Context, order *aggregate.Order) error // Delete deletes an order Delete(ctx context.Context, id string) error // NextIdentity generates the next order identity NextIdentity() string } Application Service package application import ( "context" "fmt" "myapp/internal/domain/order/aggregate" "myapp/internal/domain/order/entity" "myapp/internal/domain/order/repository" "myapp/internal/domain/order/service" "myapp/internal/domain/order/valueobject" ) // OrderService is an application service that orchestrates use cases // Application services coordinate domain objects and infrastructure type OrderService struct { orderRepo repository.OrderRepository pricingService *service.PricingService shippingCalculator *service.ShippingCalculator eventPublisher EventPublisher } // EventPublisher publishes domain events type EventPublisher interface { Publish(ctx context.Context, events []event.DomainEvent) error } // NewOrderService creates a new order service func NewOrderService( orderRepo repository.OrderRepository, pricingService *service.PricingService, shippingCalculator *service.ShippingCalculator, eventPublisher EventPublisher, ) *OrderService { return &OrderService{ orderRepo: orderRepo, pricingService: pricingService, shippingCalculator: shippingCalculator, eventPublisher: eventPublisher, } } // CreateOrderCommand represents the command to create an order type CreateOrderCommand struct { CustomerID string ShippingAddress AddressDTO BillingAddress AddressDTO Items []OrderItemDTO } // AddressDTO is a data transfer object for address type AddressDTO struct { Street string City string State string PostalCode string Country string } // OrderItemDTO is a data transfer object for order items type OrderItemDTO struct { ProductID string Product string Quantity int UnitPrice MoneyDTO } // MoneyDTO is a data transfer object for money type MoneyDTO struct { Amount int64 Currency string } // CreateOrder creates a new order (use case) func (s *OrderService) CreateOrder(ctx context.Context, cmd CreateOrderCommand) (string, error) { // Convert DTOs to value objects shippingAddr, err := valueobject.NewAddress( cmd.ShippingAddress.Street, cmd.ShippingAddress.City, cmd.ShippingAddress.State, cmd.ShippingAddress.PostalCode, cmd.ShippingAddress.Country, ) if err != nil { return "", fmt.Errorf("invalid shipping address: %w", err) } billingAddr, err := valueobject.NewAddress( cmd.BillingAddress.Street, cmd.BillingAddress.City, cmd.BillingAddress.State, cmd.BillingAddress.PostalCode, cmd.BillingAddress.Country, ) if err != nil { return "", fmt.Errorf("invalid billing address: %w", err) } // Create order aggregate orderID := s.orderRepo.NextIdentity() order, err := aggregate.NewOrder(orderID, cmd.CustomerID, shippingAddr, billingAddr) if err != nil { return "", err } // Add order lines for _, item := range cmd.Items { quantity, err := valueobject.NewQuantity(item.Quantity) if err != nil { return "", err } unitPrice, err := valueobject.NewMoney(item.UnitPrice.Amount, item.UnitPrice.Currency) if err != nil { return "", err } orderLine := entity.NewOrderLine( fmt.Sprintf("%s-line-%s", orderID, item.ProductID), item.ProductID, item.Product, quantity, unitPrice, ) if err := order.AddOrderLine(orderLine); err != nil { return "", err } } // Save order if err := s.orderRepo.Save(ctx, order); err != nil { return "", fmt.Errorf("failed to save order: %w", err) } return orderID, nil } // PlaceOrder places an order (use case) func (s *OrderService) PlaceOrder(ctx context.Context, orderID string, payment PaymentDTO) error { // Load order aggregate order, err := s.orderRepo.FindByID(ctx, orderID) if err != nil { return fmt.Errorf("order not found: %w", err) } // Create payment entity paymentAmount, err := valueobject.NewMoney(payment.Amount, payment.Currency) if err != nil { return err } paymentEntity := &entity.Payment{ // Payment entity fields } // Add payment to order if err := order.AddPayment(paymentEntity); err != nil { return err } // Place order (this enforces business rules) if err := order.PlaceOrder(); err != nil { return err } // Save order if err := s.orderRepo.Update(ctx, order); err != nil { return fmt.Errorf("failed to update order: %w", err) } // Publish domain events events := order.GetDomainEvents() if err := s.eventPublisher.Publish(ctx, events); err != nil { // Log error but don't fail the transaction fmt.Printf("failed to publish events: %v\n", err) } order.ClearDomainEvents() return nil } // PaymentDTO is a data transfer object for payment type PaymentDTO struct { Amount int64 Currency string Method string } Best Practices Ubiquitous Language: Use domain language in code and conversations Bounded Contexts: Define clear boundaries between contexts Aggregate Boundaries: Keep aggregates small and focused Immutable Value Objects: Make value objects immutable Domain Events: Use events to communicate between aggregates Repository per Aggregate: One repository per aggregate root Anemic Models: Avoid anemic domain models - put logic in entities Common Pitfalls Large Aggregates: Creating aggregates that are too large Breaking Invariants: Modifying entities without going through aggregate root Transaction Boundaries: Spanning transactions across multiple aggregates Ignoring Ubiquitous Language: Not collaborating with domain experts Over-engineering: Applying DDD to simple CRUD applications Missing Bounded Contexts: Not identifying context boundaries When to Use DDD Use When: ...

    January 22, 2025 · 18 min · Rafiul Alam

    Modular Monolith Architecture in Go: Scaling Without Microservices

    Go Architecture Patterns Series: ← Previous: Domain-Driven Design | Series Overview | Next: Microservices Architecture → What is Modular Monolith Architecture? Modular Monolith Architecture is an approach that combines the simplicity of monolithic deployment with the modularity of microservices. It organizes code into independent, loosely coupled modules with well-defined boundaries, all deployed as a single application. Key Principles: Module Independence: Each module is self-contained with its own domain logic Clear Boundaries: Modules communicate through well-defined interfaces Shared Deployment: All modules deployed together in a single process Domain Alignment: Modules organized around business capabilities Internal APIs: Modules expose APIs for inter-module communication Data Ownership: Each module owns its data and database schema Architecture Overview %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% graph TD subgraph "Modular Monolith Application" API[API Gateway/Router] subgraph "User Module" U1[User Service] U2[User Repository] U3[(User DB Schema)] end subgraph "Order Module" O1[Order Service] O2[Order Repository] O3[(Order DB Schema)] end subgraph "Product Module" P1[Product Service] P2[Product Repository] P3[(Product DB Schema)] end subgraph "Payment Module" PA1[Payment Service] PA2[Payment Repository] PA3[(Payment DB Schema)] end API --> U1 API --> O1 API --> P1 API --> PA1 U1 --> U2 O1 --> O2 P1 --> P2 PA1 --> PA2 U2 --> U3 O2 --> O3 P2 --> P3 PA2 --> PA3 O1 -.->|Module API| U1 O1 -.->|Module API| P1 O1 -.->|Module API| PA1 end style U1 fill:#1e3a5f,color:#fff style O1 fill:#78350f,color:#fff style P1 fill:#134e4a,color:#fff style PA1 fill:#4c1d95,color:#fff Module Communication Patterns %%{init: {'theme':'dark', 'themeVariables': {'primaryTextColor':'#e5e7eb','secondaryTextColor':'#e5e7eb','tertiaryTextColor':'#e5e7eb','textColor':'#e5e7eb','nodeTextColor':'#e5e7eb','edgeLabelText':'#e5e7eb','clusterTextColor':'#e5e7eb','actorTextColor':'#e5e7eb'}}}%% sequenceDiagram participant Client participant OrderModule participant UserModule participant ProductModule participant PaymentModule Client->>OrderModule: Create Order OrderModule->>UserModule: Validate User UserModule-->>OrderModule: User Valid OrderModule->>ProductModule: Check Stock ProductModule-->>OrderModule: Stock Available OrderModule->>ProductModule: Reserve Items ProductModule-->>OrderModule: Items Reserved OrderModule->>PaymentModule: Process Payment PaymentModule-->>OrderModule: Payment Success OrderModule->>ProductModule: Confirm Reservation ProductModule-->>OrderModule: Confirmed OrderModule-->>Client: Order Created Real-World Use Cases E-commerce Platforms: Product, order, inventory, and payment management SaaS Applications: Multi-tenant applications with distinct features Content Management Systems: Content, media, user, and workflow modules Banking Systems: Account, transaction, loan, and reporting modules Healthcare Systems: Patient, appointment, billing, and medical records Enterprise Applications: HR, finance, inventory, and CRM modules Modular Monolith Implementation Project Structure ├── cmd/ │ └── app/ │ └── main.go ├── internal/ │ ├── modules/ │ │ ├── user/ │ │ │ ├── domain/ │ │ │ │ ├── user.go │ │ │ │ └── repository.go │ │ │ ├── application/ │ │ │ │ └── service.go │ │ │ ├── infrastructure/ │ │ │ │ └── postgres_repository.go │ │ │ ├── api/ │ │ │ │ └── http_handler.go │ │ │ └── module.go │ │ ├── order/ │ │ │ ├── domain/ │ │ │ │ ├── order.go │ │ │ │ └── repository.go │ │ │ ├── application/ │ │ │ │ └── service.go │ │ │ ├── infrastructure/ │ │ │ │ └── postgres_repository.go │ │ │ ├── api/ │ │ │ │ └── http_handler.go │ │ │ └── module.go │ │ ├── product/ │ │ │ ├── domain/ │ │ │ │ ├── product.go │ │ │ │ └── repository.go │ │ │ ├── application/ │ │ │ │ └── service.go │ │ │ ├── infrastructure/ │ │ │ │ └── postgres_repository.go │ │ │ ├── api/ │ │ │ │ └── http_handler.go │ │ │ └── module.go │ │ └── payment/ │ │ ├── domain/ │ │ │ ├── payment.go │ │ │ └── repository.go │ │ ├── application/ │ │ │ └── service.go │ │ ├── infrastructure/ │ │ │ └── postgres_repository.go │ │ ├── api/ │ │ │ └── http_handler.go │ │ └── module.go │ └── shared/ │ ├── database/ │ │ └── postgres.go │ └── events/ │ └── event_bus.go └── go.mod Module 1: User Module // internal/modules/user/domain/user.go package domain import ( "context" "errors" "time" ) type UserID string type User struct { ID UserID Email string Name string Active bool CreatedAt time.Time UpdatedAt time.Time } var ( ErrUserNotFound = errors.New("user not found") ErrUserAlreadyExists = errors.New("user already exists") ErrInvalidEmail = errors.New("invalid email") ) // Repository defines the interface for user storage type Repository interface { Create(ctx context.Context, user *User) error GetByID(ctx context.Context, id UserID) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) Update(ctx context.Context, user *User) error Delete(ctx context.Context, id UserID) error } // internal/modules/user/application/service.go package application import ( "context" "fmt" "regexp" "app/internal/modules/user/domain" ) type Service struct { repo domain.Repository } func NewService(repo domain.Repository) *Service { return &Service{repo: repo} } func (s *Service) CreateUser(ctx context.Context, email, name string) (*domain.User, error) { if !isValidEmail(email) { return nil, domain.ErrInvalidEmail } // Check if user exists existing, _ := s.repo.GetByEmail(ctx, email) if existing != nil { return nil, domain.ErrUserAlreadyExists } user := &domain.User{ ID: domain.UserID(generateID()), Email: email, Name: name, Active: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.repo.Create(ctx, user); err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } return user, nil } func (s *Service) GetUser(ctx context.Context, id domain.UserID) (*domain.User, error) { return s.repo.GetByID(ctx, id) } func (s *Service) ValidateUser(ctx context.Context, id domain.UserID) (bool, error) { user, err := s.repo.GetByID(ctx, id) if err != nil { return false, err } return user.Active, nil } func isValidEmail(email string) bool { emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return emailRegex.MatchString(email) } func generateID() string { return fmt.Sprintf("user_%d", time.Now().UnixNano()) } // internal/modules/user/infrastructure/postgres_repository.go package infrastructure import ( "context" "database/sql" "fmt" "app/internal/modules/user/domain" ) type PostgresRepository struct { db *sql.DB } func NewPostgresRepository(db *sql.DB) *PostgresRepository { return &PostgresRepository{db: db} } func (r *PostgresRepository) Create(ctx context.Context, user *domain.User) error { query := ` INSERT INTO users.users (id, email, name, active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) ` _, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Name, user.Active, user.CreatedAt, user.UpdatedAt) return err } func (r *PostgresRepository) GetByID(ctx context.Context, id domain.UserID) (*domain.User, error) { query := ` SELECT id, email, name, active, created_at, updated_at FROM users.users WHERE id = $1 ` user := &domain.User{} err := r.db.QueryRowContext(ctx, query, id).Scan( &user.ID, &user.Email, &user.Name, &user.Active, &user.CreatedAt, &user.UpdatedAt, ) if err == sql.ErrNoRows { return nil, domain.ErrUserNotFound } return user, err } func (r *PostgresRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) { query := ` SELECT id, email, name, active, created_at, updated_at FROM users.users WHERE email = $1 ` user := &domain.User{} err := r.db.QueryRowContext(ctx, query, email).Scan( &user.ID, &user.Email, &user.Name, &user.Active, &user.CreatedAt, &user.UpdatedAt, ) if err == sql.ErrNoRows { return nil, domain.ErrUserNotFound } return user, err } func (r *PostgresRepository) Update(ctx context.Context, user *domain.User) error { query := ` UPDATE users.users SET email = $2, name = $3, active = $4, updated_at = $5 WHERE id = $1 ` _, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Name, user.Active, user.UpdatedAt) return err } func (r *PostgresRepository) Delete(ctx context.Context, id domain.UserID) error { query := `DELETE FROM users.users WHERE id = $1` _, err := r.db.ExecContext(ctx, query, id) return err } // internal/modules/user/module.go package user import ( "database/sql" "app/internal/modules/user/application" "app/internal/modules/user/infrastructure" ) type Module struct { Service *application.Service } func NewModule(db *sql.DB) *Module { repo := infrastructure.NewPostgresRepository(db) service := application.NewService(repo) return &Module{ Service: service, } } Module 2: Product Module // internal/modules/product/domain/product.go package domain import ( "context" "errors" "time" ) type ProductID string type Product struct { ID ProductID Name string Description string Price float64 Stock int CreatedAt time.Time UpdatedAt time.Time } var ( ErrProductNotFound = errors.New("product not found") ErrInsufficientStock = errors.New("insufficient stock") ErrInvalidPrice = errors.New("invalid price") ) type Repository interface { Create(ctx context.Context, product *Product) error GetByID(ctx context.Context, id ProductID) (*Product, error) Update(ctx context.Context, product *Product) error ReserveStock(ctx context.Context, id ProductID, quantity int) error ReleaseStock(ctx context.Context, id ProductID, quantity int) error } // internal/modules/product/application/service.go package application import ( "context" "fmt" "app/internal/modules/product/domain" ) type Service struct { repo domain.Repository } func NewService(repo domain.Repository) *Service { return &Service{repo: repo} } func (s *Service) CreateProduct(ctx context.Context, name, description string, price float64, stock int) (*domain.Product, error) { if price <= 0 { return nil, domain.ErrInvalidPrice } product := &domain.Product{ ID: domain.ProductID(generateID()), Name: name, Description: description, Price: price, Stock: stock, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.repo.Create(ctx, product); err != nil { return nil, fmt.Errorf("failed to create product: %w", err) } return product, nil } func (s *Service) GetProduct(ctx context.Context, id domain.ProductID) (*domain.Product, error) { return s.repo.GetByID(ctx, id) } func (s *Service) CheckStock(ctx context.Context, id domain.ProductID, quantity int) (bool, error) { product, err := s.repo.GetByID(ctx, id) if err != nil { return false, err } return product.Stock >= quantity, nil } func (s *Service) ReserveStock(ctx context.Context, id domain.ProductID, quantity int) error { available, err := s.CheckStock(ctx, id, quantity) if err != nil { return err } if !available { return domain.ErrInsufficientStock } return s.repo.ReserveStock(ctx, id, quantity) } func (s *Service) ConfirmReservation(ctx context.Context, id domain.ProductID, quantity int) error { // In a real implementation, this would mark the reservation as confirmed return nil } func generateID() string { return fmt.Sprintf("product_%d", time.Now().UnixNano()) } // internal/modules/product/infrastructure/postgres_repository.go package infrastructure import ( "context" "database/sql" "app/internal/modules/product/domain" ) type PostgresRepository struct { db *sql.DB } func NewPostgresRepository(db *sql.DB) *PostgresRepository { return &PostgresRepository{db: db} } func (r *PostgresRepository) Create(ctx context.Context, product *domain.Product) error { query := ` INSERT INTO products.products (id, name, description, price, stock, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ` _, err := r.db.ExecContext(ctx, query, product.ID, product.Name, product.Description, product.Price, product.Stock, product.CreatedAt, product.UpdatedAt) return err } func (r *PostgresRepository) GetByID(ctx context.Context, id domain.ProductID) (*domain.Product, error) { query := ` SELECT id, name, description, price, stock, created_at, updated_at FROM products.products WHERE id = $1 ` product := &domain.Product{} err := r.db.QueryRowContext(ctx, query, id).Scan( &product.ID, &product.Name, &product.Description, &product.Price, &product.Stock, &product.CreatedAt, &product.UpdatedAt, ) if err == sql.ErrNoRows { return nil, domain.ErrProductNotFound } return product, err } func (r *PostgresRepository) Update(ctx context.Context, product *domain.Product) error { query := ` UPDATE products.products SET name = $2, description = $3, price = $4, stock = $5, updated_at = $6 WHERE id = $1 ` _, err := r.db.ExecContext(ctx, query, product.ID, product.Name, product.Description, product.Price, product.Stock, product.UpdatedAt) return err } func (r *PostgresRepository) ReserveStock(ctx context.Context, id domain.ProductID, quantity int) error { query := ` UPDATE products.products SET stock = stock - $2 WHERE id = $1 AND stock >= $2 ` result, err := r.db.ExecContext(ctx, query, id, quantity) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return domain.ErrInsufficientStock } return nil } func (r *PostgresRepository) ReleaseStock(ctx context.Context, id domain.ProductID, quantity int) error { query := ` UPDATE products.products SET stock = stock + $2 WHERE id = $1 ` _, err := r.db.ExecContext(ctx, query, id, quantity) return err } // internal/modules/product/module.go package product import ( "database/sql" "app/internal/modules/product/application" "app/internal/modules/product/infrastructure" ) type Module struct { Service *application.Service } func NewModule(db *sql.DB) *Module { repo := infrastructure.NewPostgresRepository(db) service := application.NewService(repo) return &Module{ Service: service, } } Module 3: Order Module (Coordinates Other Modules) // internal/modules/order/domain/order.go package domain import ( "context" "errors" "time" "app/internal/modules/user/domain" "app/internal/modules/product/domain" ) type OrderID string type OrderStatus string const ( OrderStatusPending OrderStatus = "pending" OrderStatusConfirmed OrderStatus = "confirmed" OrderStatusCancelled OrderStatus = "cancelled" ) type OrderItem struct { ProductID domain.ProductID Quantity int Price float64 } type Order struct { ID OrderID UserID domain.UserID Items []OrderItem Total float64 Status OrderStatus CreatedAt time.Time UpdatedAt time.Time } var ( ErrOrderNotFound = errors.New("order not found") ErrInvalidOrder = errors.New("invalid order") ) type Repository interface { Create(ctx context.Context, order *Order) error GetByID(ctx context.Context, id OrderID) (*Order, error) Update(ctx context.Context, order *Order) error } // internal/modules/order/application/service.go package application import ( "context" "fmt" "time" orderdomain "app/internal/modules/order/domain" productapp "app/internal/modules/product/application" userapp "app/internal/modules/user/application" ) // Service coordinates between modules type Service struct { repo orderdomain.Repository userService *userapp.Service productService *productapp.Service } func NewService( repo orderdomain.Repository, userService *userapp.Service, productService *productapp.Service, ) *Service { return &Service{ repo: repo, userService: userService, productService: productService, } } func (s *Service) CreateOrder(ctx context.Context, userID domain.UserID, items []orderdomain.OrderItem) (*orderdomain.Order, error) { // Validate user through User module valid, err := s.userService.ValidateUser(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to validate user: %w", err) } if !valid { return nil, fmt.Errorf("user is not active") } // Calculate total and validate products var total float64 for i, item := range items { product, err := s.productService.GetProduct(ctx, item.ProductID) if err != nil { return nil, fmt.Errorf("failed to get product: %w", err) } // Check stock availability available, err := s.productService.CheckStock(ctx, item.ProductID, item.Quantity) if err != nil { return nil, fmt.Errorf("failed to check stock: %w", err) } if !available { return nil, fmt.Errorf("insufficient stock for product %s", item.ProductID) } items[i].Price = product.Price total += product.Price * float64(item.Quantity) } // Reserve stock for all items for _, item := range items { if err := s.productService.ReserveStock(ctx, item.ProductID, item.Quantity); err != nil { // Rollback reservations on failure return nil, fmt.Errorf("failed to reserve stock: %w", err) } } order := &orderdomain.Order{ ID: orderdomain.OrderID(generateID()), UserID: userID, Items: items, Total: total, Status: orderdomain.OrderStatusPending, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.repo.Create(ctx, order); err != nil { return nil, fmt.Errorf("failed to create order: %w", err) } return order, nil } func (s *Service) GetOrder(ctx context.Context, id orderdomain.OrderID) (*orderdomain.Order, error) { return s.repo.GetByID(ctx, id) } func (s *Service) ConfirmOrder(ctx context.Context, id orderdomain.OrderID) error { order, err := s.repo.GetByID(ctx, id) if err != nil { return err } // Confirm stock reservations for _, item := range order.Items { if err := s.productService.ConfirmReservation(ctx, item.ProductID, item.Quantity); err != nil { return fmt.Errorf("failed to confirm reservation: %w", err) } } order.Status = orderdomain.OrderStatusConfirmed order.UpdatedAt = time.Now() return s.repo.Update(ctx, order) } func generateID() string { return fmt.Sprintf("order_%d", time.Now().UnixNano()) } // internal/modules/order/module.go package order import ( "database/sql" "app/internal/modules/order/application" "app/internal/modules/order/infrastructure" productapp "app/internal/modules/product/application" userapp "app/internal/modules/user/application" ) type Module struct { Service *application.Service } func NewModule(db *sql.DB, userService *userapp.Service, productService *productapp.Service) *Module { repo := infrastructure.NewPostgresRepository(db) service := application.NewService(repo, userService, productService) return &Module{ Service: service, } } Main Application // cmd/app/main.go package main import ( "database/sql" "log" "net/http" _ "github.com/lib/pq" "app/internal/modules/user" "app/internal/modules/product" "app/internal/modules/order" ) func main() { // Initialize database db, err := sql.Open("postgres", "postgres://user:pass@localhost/modular_monolith?sslmode=disable") if err != nil { log.Fatal(err) } defer db.Close() // Initialize modules userModule := user.NewModule(db) productModule := product.NewModule(db) orderModule := order.NewModule(db, userModule.Service, productModule.Service) // Setup HTTP routes mux := http.NewServeMux() // User endpoints mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) { // Handle user creation }) // Product endpoints mux.HandleFunc("POST /products", func(w http.ResponseWriter, r *http.Request) { // Handle product creation }) // Order endpoints mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) { // Handle order creation using orderModule.Service }) log.Println("Server starting on :8080") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatal(err) } } Best Practices Module Boundaries: Keep modules independent with clear interfaces Shared Database: Use schemas or table prefixes to separate module data Module APIs: Define explicit APIs for inter-module communication Dependency Direction: Modules should depend on interfaces, not implementations Event-Driven Communication: Use events for async inter-module communication Transaction Management: Handle cross-module transactions carefully Testing: Test modules independently with mocked dependencies Documentation: Document module APIs and boundaries clearly Common Pitfalls Shared Models: Sharing domain models between modules creates tight coupling Direct Database Access: Modules accessing other modules’ database tables Circular Dependencies: Modules depending on each other directly Anemic Modules: Modules with no business logic, just CRUD operations God Modules: Modules that know too much about other modules Ignoring Boundaries: Calling internal implementations instead of module APIs Synchronous Coupling: Over-reliance on synchronous inter-module calls When to Use Modular Monolith Use When: ...

    January 19, 2025 · 13 min · Rafiul Alam

    Visual Guide to Distributed Systems Patterns

    Introduction Building robust distributed systems requires understanding fundamental patterns that solve common challenges like consensus, fault tolerance, request distribution, and asynchronous communication. This comprehensive guide uses visual diagrams to illustrate how these patterns work, making complex distributed systems concepts easier to understand and implement. We’ll explore: Raft Consensus Algorithm: How distributed systems agree on shared state Circuit Breaker Pattern: Preventing cascading failures in microservices Load Balancing Algorithms: Distributing traffic efficiently across servers Message Queue Patterns: Asynchronous communication strategies Part 1: Raft Consensus Algorithm The Raft consensus algorithm ensures that a cluster of servers maintains a consistent, replicated log even in the face of failures. It’s designed to be more understandable than Paxos while providing the same guarantees. ...

    January 17, 2025 · 24 min · Rafiul Alam