Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Implementing a Layered Architecture in Go: Controller, Service, and Data Access

Notes May 8 3

Effective software design often leverages layered architectures to separate concerns, making applications more modular, maintainable, and testable. The Controller, Service, and Data Access Object (DAO) pattern, sometimes referrred to as Repository, is a common approach for structuring backend applications. This design segregates the presentation logic, business rules, and data persistence operations into distinct layers, promoting a clear division of responsibilities.

Core Components of a Layered Architecture

  1. Model Layer: Defines the data structures (entities) used throughout the application.
  2. Data Access Layer (DAL) / Repository: Handles direct interaction with the data store (e.g., database, file system, external API). It abstracts data persistence operations.
  3. Business Logic Layer (Service): Encapsulates the application's business rules and workflows. It orchestrates operations between the data access layer and the presentation layer.
  4. Presentation Layer / Controller: Manages user input, translates requests, and orchestrates the flow by interacting with the service layer. In a web application, this is typically handled by HTTP controllers.

This demonstration illustrates these concepts using Go, implementing an in-memory storage solution for simplicity.

1. Model Definition

We start by defining the fundamental data structure for our application, the User entity, within an internal/models package.

// internal/models/user.go
package models

// User represents a user entity in the system.
type User struct {
    ID    int64
    Name  string
    Email string
}

2. Data Access Layer (Repository)

The Data Access Layer (DAL), often implemented as a Repository pattern, provides an abstraction over data storage mechanisms. It defines an interface for data operations and an implementation that interacts with the actual storage. Here, inMemoryUserRepository serves as a simple in-memory database.

// internal/repository/user_repository.go
package repository

import (
	"errors"
	"sync"
	"my_app/internal/models" // Adjust import path based on your module name
)

// UserRepository defines the interface for user data operations.
type UserRepository interface {
	PersistUser(user *models.User) (*models.User, error)
	FindUserByID(id int64) (*models.User, error)
}

// inMemoryUserRepository is an in-memory implementation of UserRepository.
type inMemoryUserRepository struct {
	userRecords map[int64]*models.User
	accessMutex sync.Mutex
	idSequence  int64
}

// NewInMemoryUserRepository creates a new in-memory user repository instance.
func NewInMemoryUserRepository() UserRepository {
	return &inMemoryUserRepository{
		userRecords: make(map[int64]*models.User),
		idSequence:  1,
	}
}

// PersistUser adds a new user record to the store and assigns an ID.
func (repo *inMemoryUserRepository) PersistUser(user *models.User) (*models.User, error) {
	repo.accessMutex.Lock()
	defer repo.accessMutex.Unlock()

	newUser := *user // Create a copy to prevent external modification
	newUser.ID = repo.idSequence
	repo.idSequence++
	repo.userRecords[newUser.ID] = &newUser
	return &newUser, nil
}

// FindUserByID retrieves a user record by its identifier.
func (repo *inMemoryUserRepository) FindUserByID(id int64) (*models.User, error) {
	repo.accessMutex.Lock()
	defer repo.accessMutex.Unlock()

	if record, exists := repo.userRecords[id]; exists {
		return record, nil
	}
	return nil, errors.New("user record not found")
}

3. Business Logic Layer (Service)

The Service Layer contains the core business rules and logic. It interacts with the Data Access Layer to perform operations and applies any necessary validations or transformations. It's unaware of how data is stored or how requests are received.

// internal/service/user_service.go
package service

import (
	"errors"
	"fmt"
	"my_app/internal/models"      // Adjust import path
	"my_app/internal/repository" // Adjust import path
)

// UserManagementService defines operations related to user business logic.
type UserManagementService interface {
	RegisterNewUser(name, email string) (*models.User, error)
	RetrieveUserDetails(userID int64) (*models.User, error)
}

// defaultUserManagementService is the concrete implementation of UserManagementService.
type defaultUserManagementService struct {
	userRepo repository.UserRepository
}

// NewUserManagementService creates a new instance of the user management service.
func NewUserManagementService(repo repository.UserRepository) UserManagementService {
	return &defaultUserManagementService{userRepo: repo}
}

// RegisterNewUser handles the business logic for creating a new user.
func (svc *defaultUserManagementService) RegisterNewUser(name, email string) (*models.User, error) {
	if name == "" || email == "" {
		return nil, errors.New("name and email cannot be empty")
	}
	// Additional business logic like email format validation, uniqueness check can go here.

	user := &models.User{Name: name, Email: email}
	createdUser, err := svc.userRepo.PersistUser(user)
	if err != nil {
		return nil, fmt.Errorf("failed to save user: %w", err)
	}
	return createdUser, nil
}

// RetrieveUserDetails fetches user details by ID, applying any necessary business rules.
func (svc *defaultUserManagementService) RetrieveUserDetails(userID int64) (*models.User, error) {
	if userID <= 0 {
		return nil, errors.New("invalid user ID provided")
	}
	user, err := svc.userRepo.FindUserByID(userID)
	if err != nil {
		return nil, fmt.Errorf("could not find user: %w", err)
	}
	return user, nil
}

4. Presentation Layer (Controller / Application Entrypoint)

The Presentation Layer, often a Controller in a web context, is the entry point for requests. It receives input, delegates to the Service Layer for business processing, and formats the response. In this console aplication example, main.go serves as the entrypoint that orchestrates interactions.

// cmd/my_app/main.go
package main

import (
	"fmt"
	"my_app/internal/repository" // Adjust import path
	"my_app/internal/service"    // Adjust import path
)

func main() {
	// Initialize the data access layer (Repository)
	userRepo := repository.NewInMemoryUserRepository()

	// Initialize the business service layer, injecting the repository dependency
	userSvc := service.NewUserManagementService(userRepo)

	// --- Simulate a request to register a new user ---
	fmt.Println("--- Registering New User ---")
	registeredUser, err := userSvc.RegisterNewUser("Jane Doe", "jane.doe@example.com")
	if err != nil {
		fmt.Printf("Error during user registration: %v\n", err)
		return
	}
	fmt.Printf("User registered successfully: ID=%d, Name='%s', Email='%s'\n",
		registeredUser.ID, registeredUser.Name, registeredUser.Email)

	// --- Simulate a request to retrieve user details by ID ---
	fmt.Println("\n--- Retrieving User Details ---")
	fetchedUser, err := userSvc.RetrieveUserDetails(registeredUser.ID)
	if err != nil {
		fmt.Printf("Error retrieving user: %v\n", err)
		return
	}
	fmt.Printf("User details fetched: ID=%d, Name='%s', Email='%s'\n",
		fetchedUser.ID, fetchedUser.Name, fetchedUser.Email)

	// --- Demonstrate error handling: user not found ---
	fmt.Println("\n--- Demonstrating User Not Found ---")
	_, err = userSvc.RetrieveUserDetails(999) // Non-existent ID
	if err != nil {
		fmt.Printf("Expected error for non-existent user: %v\n", err)
	}

	// --- Demonstrate error handling: invalid input ---
	fmt.Println("\n--- Demonstrating Invalid Input ---")
	_, err = userSvc.RegisterNewUser("", "") // Empty name and email
	if err != nil {
		fmt.Printf("Expected error for invalid input: %v\n", err)
	}
}

This demonstration highlights how the Controller, Service, and Data Access (Repository) layers work togetheer in a Go application. Each layer focuses on its specific responsibilities, promoting a clean separation of concerns, which results in more organized, maintainable, and testable code.

Tags: go

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.