Go Architecture Patterns Series: Series Overview | Next: Clean Architecture →
What is Layered Architecture?
Layered Architecture is one of the most common and fundamental architectural patterns in software development. It organizes code into horizontal layers, where each layer has a specific responsibility and only communicates with adjacent layers.
Key Principles:
- Separation of Concerns: Each layer handles a specific aspect of the application
- Layer Independence: Layers are loosely coupled and can be changed independently
- Unidirectional Dependencies: Dependencies flow in one direction (top-down)
- Clear Boundaries: Well-defined interfaces between layers
- Testability: Each layer can be tested in isolation
Architecture Overview
graph TD
A[Presentation Layer] --> B[Business Logic Layer]
B --> C[Data Access Layer]
C --> D[(Database)]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#e8f5e9
style D fill:#f3e5f5
Traditional 3-Tier Layered Architecture
graph LR
subgraph "Presentation Layer"
A1[HTTP Handlers]
A2[Templates/Views]
A3[Request/Response DTOs]
end
subgraph "Business Logic Layer"
B1[Services]
B2[Business Rules]
B3[Domain Models]
end
subgraph "Data Access Layer"
C1[Repositories]
C2[Database Access]
C3[Data Models]
end
A1 --> B1
A2 --> B1
B1 --> C1
C1 --> C2
style A1 fill:#e1f5ff
style B1 fill:#fff4e1
style C1 fill:#e8f5e9
Real-World Use Cases
- Web Applications: RESTful APIs and web services
- Enterprise Applications: Business management systems
- CRUD Applications: Standard create-read-update-delete operations
- Monolithic Applications: Traditional single-deployment applications
- Internal Tools: Admin panels and dashboards
- Legacy System Modernization: Refactoring existing codebases
Basic Layered Architecture Implementation
Project Structure
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── handlers/ # Presentation Layer
│ │ ├── user_handler.go
│ │ └── product_handler.go
│ ├── services/ # Business Logic Layer
│ │ ├── user_service.go
│ │ └── product_service.go
│ ├── repositories/ # Data Access Layer
│ │ ├── user_repository.go
│ │ └── product_repository.go
│ └── models/ # Domain Models
│ ├── user.go
│ └── product.go
└── go.mod
Layer 1: Domain Models
package models
import "time"
// User represents a user in the system
type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Product represents a product in the system
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Order represents an order in the system
type Order struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
ProductID int64 `json:"product_id"`
Quantity int `json:"quantity"`
Total float64 `json:"total"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
Layer 2: Data Access Layer (Repository)
package repositories
import (
"context"
"database/sql"
"fmt"
"time"
"myapp/internal/models"
)
// UserRepository handles user data access
type UserRepository struct {
db *sql.DB
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// Create creates a new user
func (r *UserRepository) Create(ctx context.Context, user *models.User) error {
query := `
INSERT INTO users (email, name, created_at, updated_at)
VALUES ($1, $2, $3, $4)
RETURNING id
`
now := time.Now()
user.CreatedAt = now
user.UpdatedAt = now
err := r.db.QueryRowContext(
ctx, query,
user.Email, user.Name, user.CreatedAt, user.UpdatedAt,
).Scan(&user.ID)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
// GetByID retrieves a user by ID
func (r *UserRepository) GetByID(ctx context.Context, id int64) (*models.User, error) {
query := `
SELECT id, email, name, created_at, updated_at
FROM users
WHERE id = $1
`
user := &models.User{}
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user not found")
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// GetByEmail retrieves a user by email
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) {
query := `
SELECT id, email, name, created_at, updated_at
FROM users
WHERE email = $1
`
user := &models.User{}
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user not found")
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// Update updates a user
func (r *UserRepository) Update(ctx context.Context, user *models.User) error {
query := `
UPDATE users
SET email = $1, name = $2, updated_at = $3
WHERE id = $4
`
user.UpdatedAt = time.Now()
result, err := r.db.ExecContext(
ctx, query,
user.Email, user.Name, user.UpdatedAt, user.ID,
)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return fmt.Errorf("user not found")
}
return nil
}
// Delete deletes a user
func (r *UserRepository) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return fmt.Errorf("user not found")
}
return nil
}
// List retrieves all users
func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) {
query := `
SELECT id, email, name, created_at, updated_at
FROM users
ORDER BY id DESC
LIMIT $1 OFFSET $2
`
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
defer rows.Close()
var users []*models.User
for rows.Next() {
user := &models.User{}
err := rows.Scan(
&user.ID,
&user.Email,
&user.Name,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, user)
}
return users, nil
}
// ProductRepository handles product data access
type ProductRepository struct {
db *sql.DB
}
// NewProductRepository creates a new product repository
func NewProductRepository(db *sql.DB) *ProductRepository {
return &ProductRepository{db: db}
}
// UpdateStock updates product stock
func (r *ProductRepository) UpdateStock(ctx context.Context, productID int64, quantity int) error {
query := `
UPDATE products
SET stock = stock + $1, updated_at = $2
WHERE id = $3
`
result, err := r.db.ExecContext(ctx, query, quantity, time.Now(), productID)
if err != nil {
return fmt.Errorf("failed to update stock: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return fmt.Errorf("product not found")
}
return nil
}
Layer 3: Business Logic Layer (Service)
package services
import (
"context"
"fmt"
"regexp"
"myapp/internal/models"
"myapp/internal/repositories"
)
// UserService handles user business logic
type UserService struct {
userRepo *repositories.UserRepository
}
// NewUserService creates a new user service
func NewUserService(userRepo *repositories.UserRepository) *UserService {
return &UserService{
userRepo: userRepo,
}
}
// CreateUser creates a new user with validation
func (s *UserService) CreateUser(ctx context.Context, email, name string) (*models.User, error) {
// Business rule: Validate email format
if !isValidEmail(email) {
return nil, fmt.Errorf("invalid email format")
}
// Business rule: Name must not be empty
if name == "" {
return nil, fmt.Errorf("name cannot be empty")
}
// Business rule: Email must be unique
existingUser, err := s.userRepo.GetByEmail(ctx, email)
if err == nil && existingUser != nil {
return nil, fmt.Errorf("email already exists")
}
user := &models.User{
Email: email,
Name: name,
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
// GetUser retrieves a user by ID
func (s *UserService) GetUser(ctx context.Context, id int64) (*models.User, error) {
return s.userRepo.GetByID(ctx, id)
}
// UpdateUser updates a user with validation
func (s *UserService) UpdateUser(ctx context.Context, id int64, email, name string) (*models.User, error) {
// Business rule: Validate email format
if !isValidEmail(email) {
return nil, fmt.Errorf("invalid email format")
}
// Business rule: Name must not be empty
if name == "" {
return nil, fmt.Errorf("name cannot be empty")
}
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Business rule: If email is changing, check uniqueness
if user.Email != email {
existingUser, err := s.userRepo.GetByEmail(ctx, email)
if err == nil && existingUser != nil {
return nil, fmt.Errorf("email already exists")
}
}
user.Email = email
user.Name = name
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return user, nil
}
// DeleteUser deletes a user
func (s *UserService) DeleteUser(ctx context.Context, id int64) error {
return s.userRepo.Delete(ctx, id)
}
// ListUsers retrieves users with pagination
func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]*models.User, error) {
// Business rule: Validate pagination parameters
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
offset := (page - 1) * pageSize
return s.userRepo.List(ctx, pageSize, offset)
}
// isValidEmail validates email format
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// OrderService handles order business logic
type OrderService struct {
userRepo *repositories.UserRepository
productRepo *repositories.ProductRepository
}
// NewOrderService creates a new order service
func NewOrderService(
userRepo *repositories.UserRepository,
productRepo *repositories.ProductRepository,
) *OrderService {
return &OrderService{
userRepo: userRepo,
productRepo: productRepo,
}
}
// PlaceOrder places a new order with business validation
func (s *OrderService) PlaceOrder(ctx context.Context, userID, productID int64, quantity int) (*models.Order, error) {
// Business rule: Validate user exists
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Business rule: Validate quantity
if quantity <= 0 {
return nil, fmt.Errorf("quantity must be positive")
}
// Business rule: Check stock availability
// (In a real implementation, this would be in ProductRepository)
// Business rule: Calculate total
// (In a real implementation, this would fetch product price)
order := &models.Order{
UserID: user.ID,
ProductID: productID,
Quantity: quantity,
Status: "pending",
}
return order, nil
}
Layer 4: Presentation Layer (HTTP Handlers)
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"myapp/internal/services"
)
// UserHandler handles HTTP requests for users
type UserHandler struct {
userService *services.UserService
}
// NewUserHandler creates a new user handler
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{
userService: userService,
}
}
// CreateUserRequest represents the request to create a user
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
// CreateUser handles POST /users
func (h *UserHandler) 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 body")
return
}
user, err := h.userService.CreateUser(r.Context(), req.Email, req.Name)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
// GetUser handles GET /users/:id
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL (implementation depends on router)
id, err := extractIDFromPath(r)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user ID")
return
}
user, err := h.userService.GetUser(r.Context(), id)
if err != nil {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, user)
}
// UpdateUser handles PUT /users/:id
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := extractIDFromPath(r)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user ID")
return
}
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := h.userService.UpdateUser(r.Context(), id, req.Email, req.Name)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, user)
}
// DeleteUser handles DELETE /users/:id
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
id, err := extractIDFromPath(r)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user ID")
return
}
if err := h.userService.DeleteUser(r.Context(), id); err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
// ListUsers handles GET /users
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
users, err := h.userService.ListUsers(r.Context(), page, pageSize)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to list users")
return
}
respondJSON(w, http.StatusOK, users)
}
// Helper functions
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})
}
func extractIDFromPath(r *http.Request) (int64, error) {
// This is a simplified version - use your router's method
// For example with chi: chi.URLParam(r, "id")
return 1, nil
}
Main Application
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/lib/pq"
"myapp/internal/handlers"
"myapp/internal/repositories"
"myapp/internal/services"
)
func main() {
// Initialize database connection
db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize layers from bottom to top
// Data Access Layer
userRepo := repositories.NewUserRepository(db)
productRepo := repositories.NewProductRepository(db)
// Business Logic Layer
userService := services.NewUserService(userRepo)
orderService := services.NewOrderService(userRepo, productRepo)
// Presentation Layer
userHandler := handlers.NewUserHandler(userService)
// Setup routes
mux := http.NewServeMux()
mux.HandleFunc("POST /users", userHandler.CreateUser)
mux.HandleFunc("GET /users/{id}", userHandler.GetUser)
mux.HandleFunc("PUT /users/{id}", userHandler.UpdateUser)
mux.HandleFunc("DELETE /users/{id}", userHandler.DeleteUser)
mux.HandleFunc("GET /users", userHandler.ListUsers)
// Start server
log.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
Testing Layered Architecture
package services_test
import (
"context"
"testing"
"myapp/internal/models"
"myapp/internal/services"
)
// MockUserRepository for testing
type MockUserRepository struct {
users map[int64]*models.User
nextID int64
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int64]*models.User),
nextID: 1,
}
}
func (m *MockUserRepository) Create(ctx context.Context, user *models.User) error {
user.ID = m.nextID
m.nextID++
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*models.User, error) {
user, exists := m.users[id]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) {
for _, user := range m.users {
if user.Email == email {
return user, nil
}
}
return nil, fmt.Errorf("user not found")
}
func TestUserService_CreateUser(t *testing.T) {
mockRepo := NewMockUserRepository()
service := services.NewUserService(mockRepo)
tests := []struct {
name string
email string
userName string
wantErr bool
}{
{
name: "valid user",
email: "[email protected]",
userName: "Test User",
wantErr: false,
},
{
name: "invalid email",
email: "invalid-email",
userName: "Test User",
wantErr: true,
},
{
name: "empty name",
email: "[email protected]",
userName: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := service.CreateUser(context.Background(), tt.email, tt.userName)
if tt.wantErr {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if user.Email != tt.email {
t.Errorf("expected email %s, got %s", tt.email, user.Email)
}
})
}
}
Best Practices
- Clear Layer Boundaries: Define clear interfaces between layers
- Dependency Direction: Always depend downward, never upward
- Keep Layers Thin: Avoid bloated service layers with too much logic
- Use Dependency Injection: Inject dependencies rather than creating them
- Interface Segregation: Define minimal interfaces for layer communication
- Error Handling: Handle errors appropriately at each layer
- Testing: Test each layer independently with mocks
Common Pitfalls
- Layer Violation: Skipping layers (e.g., handler directly accessing repository)
- Anemic Domain Models: Models with no behavior, only data
- Fat Service Layer: Putting all logic in the service layer
- Tight Coupling: Layers knowing too much about each other
- God Objects: Services or repositories handling too many responsibilities
- Inconsistent Abstractions: Different patterns in different layers
When to Use Layered Architecture
Use When:
- Building standard CRUD applications
- Team is familiar with traditional architectures
- Requirements are straightforward and stable
- Need quick development with proven patterns
- Building monolithic applications
Avoid When:
- Building complex domain-driven systems (use DDD)
- Need high flexibility and testability (use Clean/Hexagonal)
- Building microservices (consider other patterns)
- Domain logic is complex and constantly changing
Advantages
- Simple to Understand: Intuitive structure for most developers
- Clear Separation: Each layer has distinct responsibilities
- Easy to Test: Layers can be tested independently
- Maintainable: Changes are localized to specific layers
- Industry Standard: Well-documented with many examples
Disadvantages
- Tight Coupling: Layers are coupled to adjacent layers
- Rigid Structure: Can be inflexible for complex domains
- Database-Centric: Often leads to database-driven design
- Scalability Issues: Not optimized for distributed systems
- Testing Challenges: May require database for integration tests
Layered Architecture provides a solid foundation for building maintainable applications. While it may not be suitable for all scenarios, it remains one of the most practical and widely-used architectural patterns in software development.
Go Architecture Patterns Series: Series Overview | Next: Clean Architecture →