DEV Community

Cover image for MVC vs DDD: Go Language Architecture Deep Dive
Leapcell
Leapcell

Posted on

MVC vs DDD: Go Language Architecture Deep Dive

Cover

Detailed Comparison of Go Language MVC and DDD Layered Architectures

MVC and DDD are two popular layered architectural concepts in backend development. MVC (Model-View-Controller) is a design pattern mainly used to separate user interface, business logic, and data models for easier decoupling and layering, while DDD (Domain-Driven Design) is an architectural methodology aimed at solving design and maintenance difficulties in complex systems by building business domain models.

In the Java ecosystem, many systems have gradually transitioned from MVC to DDD. However, in languages like Go, Python, and NodeJS—which advocate simplicity and efficiency—MVC remains the mainstream architecture. Below, we will specifically discuss the differences in directory structure between MVC and DDD based on Go language.

MVC Diagram Structure

+------------------+
|      View        | User Interface Layer: responsible for data display and user interaction (such as HTML pages, API responses)
+------------------+
|   Controller     | Controller Layer: processes user requests, calls Service logic, coordinates Model and View
+------------------+
|      Model       | Model Layer: contains data objects (such as database table structures) and some business logic (often scattered in Service layer)
+------------------+
Enter fullscreen mode Exit fullscreen mode

DDD Diagram Structure

+--------------------+
|   User Interface   | Responsible for user interaction and display (such as REST API, Web interface)
+--------------------+
| Application Layer  | Orchestrates business processes (such as calling domain services, transaction management), does not contain core business rules
+--------------------+
|   Domain Layer     | Core business logic layer: contains aggregate roots, entities, value objects, domain services, etc., encapsulates business rules
+--------------------+
| Infrastructure     | Provides technical implementations (such as database access, message queues, external APIs)
+--------------------+
Enter fullscreen mode Exit fullscreen mode

Main Differences Between MVC and DDD

  1. Code Organization Logic
  • MVC layers by technical function (Controller/Service/DAO), focusing on technical implementation.
  • DDD divides modules by business domain (such as order domain, payment domain), isolating core business logic through bounded contexts.
  1. Carrier of Business Logic
  • MVC usually adopts an anemic model, separating data (Model) and behavior (Service), which leads to high maintenance cost due to dispersed logic.
  • DDD achieves a rich model through aggregate roots and domain services, concentrating business logic in the domain layer and enhancing scalability.
  1. Applicability and Cost
  • MVC has a low development cost and is suitable for small to medium systems with stable requirements.
  • DDD requires upfront domain modeling and a unified language, making it suitable for large systems with complex business and long-term evolution needs, but the team must have domain abstraction capabilities. For example, in e-commerce promotion rules, DDD can prevent logic from being scattered across multiple services.

Go Language MVC Directory Structure

MVC is mainly divided into three layers: view, controller, and model.

gin-order/
├── cmd
│   └── main.go                  # Application entry point, starts the Gin engine
├── internal
│   ├── controllers              # Controller layer (handles HTTP requests), also known as handlers
│   │   └── order
│   │       └── order_controller.go  # Controller for the Order module
│   ├── services                 # Service layer (handles business logic)
│   │   └── order
│   │       └── order_service.go       # Service implementation for the Order module
│   ├── repository               # Data access layer (interacts with the database)
│   │   └── order
│   │       └── order_repository.go    # Data access interface and implementation for Order module
│   ├── models                   # Model layer (data structure definitions)
│   │   └── order
│   │       └── order.go               # Data model for the Order module
│   ├── middleware               # Middleware (such as authentication, logging, request interception)
│   │   ├── logging.go             # Logging middleware
│   │   └── auth.go                # Authentication middleware
│   └── config                   # Configuration module (database, server configurations, etc.)
│       └── config.go                # Application and environment configurations
├── pkg                          # Common utility packages (such as response wrappers)
│   └── response.go              # Response handling utility methods
├── web                          # Frontend resources (templates and static assets)
│   ├── static                   # Static resources (CSS, JS, images)
│   └── templates                # Template files (HTML templates)
│       └── order.tmpl           # View template for the Order module (if rendering HTML is needed)
├── go.mod                       # Go module management file
└── go.sum                       # Go module dependency lock file
Enter fullscreen mode Exit fullscreen mode

Go Language DDD Directory Structure

DDD is mainly divided into four layers: interface, application, domain, and infrastructure.

go-web/
│── cmd/
│   └── main.go               # Application entry point
│── internal/
│   ├── application/          # Application layer (coordinates domain logic, handles use cases)
│   │   ├── services/         # Service layer, business logic directory
│   │   │   └── order_service.go # Order application service, calls domain layer business logic
│   ├── domain/               # Domain layer (core business logic and interface definitions)
│   │   ├── order/            # Order aggregate
│   │   │   ├── order.go      # Order entity (aggregate root), contains core business logic
│   │   ├── repository/       # General repository interfaces
│   │   │   ├── repository.go # General repository interface (CRUD operations)
│   │   │   └── order_repository.go # Order repository interface, defines operations on order data
│   ├── infrastructure/       # Infrastructure layer (implements interfaces defined in the domain layer)
│   │   ├── repository/       # Repository implementation
│   │   │   └── order_repository_impl.go  # Order repository implementation, concrete order data storage
│   └── interfaces/           # Interface layer (handles external requests, such as HTTP interfaces)
│   │   ├── handlers/         # HTTP handlers
│   │   │  └── order_handler.go # HTTP handler for orders
│   │   └── routes/
│   │   │   ├── router.go     # Base router utility setup
│   │   │   └── order-routes.go # Order routes configuration
│   │   │   └── order-routes-test.go # Order routes test
│   └── middleware/           # Middleware (e.g.: authentication, interception, authorization, etc.)
│   │   └── logging.go        # Logging middleware
│   ├── config/               # Service-related configuration
│   │   └── server_config.go  # Server configuration (e.g., port, timeout settings, etc.)
│── pkg/                      # Reusable public libraries
│   └── utils/                # Utility classes (e.g.: logging, date handling, etc.)
Enter fullscreen mode Exit fullscreen mode

Go Language MVC Code Implementation

Controller (Interface Layer) → Service (Business Logic Layer) → Repository (Data Access Layer) → Model (Data Model)
Enter fullscreen mode Exit fullscreen mode

Layered Code

Controller Layer

// internal/controller/order/order.go
package order

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "github.com/gin-order/internal/model"
    "github.com/gin-order/internal/service/order"
    "github.com/gin-order/internal/pkg/response"
)

type OrderController struct {
    service *order.OrderService
}

func NewOrderController(service *order.OrderService) *OrderController {
    return &OrderController{service: service}
}

func (c *OrderController) GetOrder(ctx *gin.Context) {
    idStr := ctx.Param("id")
    id, _ := strconv.ParseUint(idStr, 10, 64)

    order, err := c.service.GetOrderByID(uint(id))
    if err != nil {
        response.Error(ctx, http.StatusNotFound, "Order not found")
        return
    }

    response.Success(ctx, order)
}

func (c *OrderController) CreateOrder(ctx *gin.Context) {
    var req model.Order
    if err := ctx.ShouldBindJSON(&req); err != nil {
        response.Error(ctx, http.StatusBadRequest, "Invalid request")
        return
    }

    if err := c.service.CreateOrder(&req); err != nil {
        response.Error(ctx, http.StatusInternalServerError, "Create failed")
        return
    }

    response.Success(ctx, req)
}
Enter fullscreen mode Exit fullscreen mode

Route Configuration

// cmd/server/main.go
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-order/internal/controller/order"
    "github.com/gin-order/internal/pkg/database"
    "github.com/gin-order/internal/repository/order"
    "github.com/gin-order/internal/service/order"
)

func main() {
    // Initialize database
    db := database.NewGORM()

    // Dependency injection
    orderRepo := order_repo.NewMySQLOrderRepository(db)
    orderService := order_service.NewOrderService(orderRepo)
    orderController := order_controller.NewOrderController(orderService)

    // Create router
    r := gin.Default()

    // Register middleware
    r.Use(middleware.Logger())

    // Route groups
    apiGroup := r.Group("/api")
    {
        orderGroup := apiGroup.Group("/orders")
        {
            orderGroup.GET("/:id", orderController.GetOrder)
            orderGroup.POST("", orderController.CreateOrder)
        }
    }

    // Start service
    r.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

Service Layer

// internal/service/order/service.go
package order

import (
    "github.com/gin-order/internal/model"
    "github.com/gin-order/internal/repository/order"
)

type OrderService struct {
    repo order.OrderRepository
}

func NewOrderService(repo order.OrderRepository) *OrderService {
    return &OrderService{repo: repo}
}

func (s *OrderService) GetOrderByID(id uint) (*model.Order, error) {
    return s.repo.FindByID(id)
}

func (s *OrderService) CreateOrder(order *model.Order) error {
    return s.repo.Create(order)
}
Enter fullscreen mode Exit fullscreen mode

Data Access Layer (Repository)

// internal/repository/order/interface.go
package order

import "github.com/gin-order/internal/model"

type OrderRepository interface {
    FindByID(id uint) (*model.Order, error)
    Create(order *model.Order) error
    FindByStatus(status string) ([]model.Order, error)
}

// internal/repository/order/mysql.go
package order

import (
    "gorm.io/gorm"
    "github.com/gin-order/internal/model"
)

type MySQLOrderRepository struct {
    db *gorm.DB
}

func NewMySQLOrderRepository(db *gorm.DB) OrderRepository {
    return &MySQLOrderRepository{db: db}
}

func (r *MySQLOrderRepository) FindByID(id uint) (*model.Order, error) {
    var order model.Order
    if err := r.db.First(&order, id).Error; err != nil {
        return nil, err
    }
    return &order, nil
}

func (r *MySQLOrderRepository) Create(order *model.Order) error {
    return r.db.Create(order).Error
}

func (r *MySQLOrderRepository) FindByStatus(status string) ([]model.Order, error) {
    var orders []model.Order
    if err := r.db.Where("status = ?", status).Find(&orders).Error; err != nil {
        return nil, err
    }
    return orders, nil
}
Enter fullscreen mode Exit fullscreen mode

Model Layer

// internal/model/order.go
package model

import "time"

type Order struct {
    OrderID     uint      `gorm:"primaryKey;column:order_id"`
    OrderNo     string    `gorm:"uniqueIndex;column:order_no"`
    UserID      uint      `gorm:"index;column:user_id"`
    OrderName   string    `gorm:"column:order_name"`
    Amount      float64   `gorm:"type:decimal(10,2);column:amount"`
    Status      string    `gorm:"column:status"`
    CreatedAt   time.Time `gorm:"column:created_at"`
    UpdatedAt   time.Time `gorm:"column:updated_at"`
}

func (Order) TableName() string {
    return "orders"
}
Enter fullscreen mode Exit fullscreen mode

Go Language MVC Best Practices

Interface Segregation Principle

The Repository layer defines interfaces, supporting multiple database implementations.

// Easily switch to a Mock implementation
type MockOrderRepository struct {}
func (m *MockOrderRepository) FindByID(id uint) (*model.Order, error) {
    return &model.Order{OrderNo: "mock-123"}, nil
}
Enter fullscreen mode Exit fullscreen mode

Unified Response Format

// pkg/response/response.go
func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, gin.H{
        "code":    0,
        "message": "success",
        "data":    data,
    })
}
Enter fullscreen mode Exit fullscreen mode

Middleware Chain

// Global middleware
r.Use(gin.Logger(), gin.Recovery())

// Route group middleware
adminGroup := r.Group("/admin", middleware.AuthJWT())
Enter fullscreen mode Exit fullscreen mode

Database Migration

Using GORM AutoMigrate:

db.AutoMigrate(&model.Order{})
Enter fullscreen mode Exit fullscreen mode

Go Language DDD Code Implementation and Best Practices

Focus on Domain Model

DDD emphasizes the construction of domain models, organizing business logic using Aggregates, Entities, and Value Objects.

In Go, entities and value objects are typically defined with struct:

// Entity
type User struct {
    ID   int
    Name string
}
Enter fullscreen mode Exit fullscreen mode

Layered Architecture

DDD typically adopts a layered architecture. Go projects can follow this structure:

  • Domain Layer: Core business logic, e.g., entities and aggregates under the domain directory.
  • Application Layer: Use cases and orchestration of business processes.
  • Infrastructure Layer: Adapters for database, caching, external APIs, etc.
  • Interface Layer: Provides HTTP, gRPC, or CLI interfaces.

Dependency Inversion

The domain layer should not directly depend on the infrastructure layer; instead, it relies on interfaces for dependency inversion.

Note: The core of DDD architecture is dependency inversion (DIP). The Domain is the innermost core, defining only business rules and interface abstractions. Other layers depend on the Domain for implementation, but the Domain does not depend on any external implementations. In Hexagonal Architecture, the domain layer sits at the core, while other layers (such as application, infrastructure) provide concrete technical details (like database operations, API calls) by implementing interfaces defined by the domain, achieving decoupling between domain and technical implementation.

// Domain layer: defines interface
type UserRepository interface {
    GetByID(id int) (*User, error)
}
Enter fullscreen mode Exit fullscreen mode
// Infrastructure layer: database implementation
type userRepositoryImpl struct {
    db *sql.DB
}

func (r *userRepositoryImpl) GetByID(id int) (*User, error) {
    // Database query logic
}
Enter fullscreen mode Exit fullscreen mode

Aggregate Management

The aggregate root manages the lifecycle of the entire aggregate:

type Order struct {
    ID      int
    Items   []OrderItem
    Status  string
}

func (o *Order) AddItem(item OrderItem) {
    o.Items = append(o.Items, item)
}
Enter fullscreen mode Exit fullscreen mode

Application Service

Application services encapsulate domain logic, preventing external layers from directly manipulating domain objects:

type OrderService struct {
    repo OrderRepository
}

func (s *OrderService) CreateOrder(userID int, items []OrderItem) (*Order, error) {
    order := Order{UserID: userID, Items: items, Status: "Pending"}
    return s.repo.Save(order)
}
Enter fullscreen mode Exit fullscreen mode

Event-Driven

Domain events are used for decoupling. In Go, you can implement this via Channels or Pub/Sub:

type OrderCreatedEvent struct {
    OrderID int
}

func publishEvent(event OrderCreatedEvent) {
    go func() {
        eventChannel <- event
    }()
}
Enter fullscreen mode Exit fullscreen mode

Combining CQRS (Command Query Responsibility Segregation)

DDD can be combined with CQRS. In Go, you can use Command for change operations and Query for data reading:

type CreateOrderCommand struct {
    UserID int
    Items  []OrderItem
}

func (h *OrderHandler) Handle(cmd CreateOrderCommand) (*Order, error) {
    return h.service.CreateOrder(cmd.UserID, cmd.Items)
}
Enter fullscreen mode Exit fullscreen mode

Summary: MVC vs. DDD Architecture

Core Differences in Architecture

MVC Architecture

  • Layers: Three layers—Controller/Service/DAO
  • Responsibilities:

    • Controller handles requests, Service contains logic
    • DAO directly operates the database
  • Pain Points: The Service layer becomes bloated, and business logic is coupled with data operations

DDD Architecture

  • Layers: Four layers—Interface Layer / Application Layer / Domain Layer / Infrastructure Layer
  • Responsibilities:

    • Application Layer orchestrates processes (e.g., calls domain services)
    • Domain Layer encapsulates business atomic operations (e.g., order creation rules)
    • Infrastructure Layer implements technical details (e.g., database access)
  • Pain Points: The domain layer is independent of technical implementations, and logic corresponds closely with the layer structure

Modularity and Scalability

MVC:

  • High Coupling: Lacks clear business boundaries; cross-module calls (e.g., order service directly relying on account tables) make code hard to maintain.
  • Poor Scalability: Adding new features requires global changes (e.g., adding risk control rules must intrude into order service), easily causing cascading issues.

DDD:

  • Bounded Context: Modules are divided by business capabilities (e.g., payment domain, risk control domain); event-driven collaboration (e.g., order payment completed event) is used for decoupling.
  • Independent Evolution: Each domain module can be upgraded independently (e.g., payment logic optimization does not affect order service), reducing system-level risks.

Applicable Scenarios

  • Prefer MVC for small to medium systems: Simple business (e.g., blogs, CMS, admin backends), requiring rapid development with clear business rules and no frequent changes.
  • Prefer DDD for complex business: Rule-intensive (e.g., financial transactions, supply chain), multi-domain collaboration (e.g., e-commerce order and inventory linkage), frequent changes in business requirements.

We are Leapcell, your top choice for hosting Go projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (1)

Collapse
 
richmirks profile image
Richard Mirks

Could you explain more about how bounded contexts are defined and enforced in a Go DDD project? Are there practical examples of boundaries between domains?