Comparing MVC and DDD Layered Architectures in Go: A Detailed Guide
James Reed
Infrastructure Engineer · Leapcell

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)
+------------------+
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)
+--------------------+
Main Differences Between MVC and DDD
-
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.
-
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.
-
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
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.)
Go Language MVC Code Implementation
Controller (Interface Layer) → Service (Business Logic Layer) → Repository (Data Access Layer) → Model (Data Model)
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) }
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") }
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) }
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 }
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" }
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 }
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, }) }
Middleware Chain
// Global middleware r.Use(gin.Logger(), gin.Recovery()) // Route group middleware adminGroup := r.Group("/admin", middleware.AuthJWT())
Database Migration
Using GORM AutoMigrate:
db.AutoMigrate(&model.Order{})
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 }
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) }
// Infrastructure layer: database implementation type userRepositoryImpl struct { db *sql.DB } func (r *userRepositoryImpl) GetByID(id int) (*User, error) { // Database query logic }
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) }
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) }
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 }() }
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) }
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 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!
Follow us on X: @LeapcellHQ