Vjerci.com

A personal blog by software engineer

January, 3-nd, 2026

Clean Architecture -> Separation of concerns



This tutorial series assumes you are familiar with golang, as we will be using it thorough this article series.


About Separation of concerns

General rule is: Things that were implemented in one part of the system should not concern or be implemented in another part of the system.

As to why: It provides better system separation, which means that changes in one part of the system will not harm other parts in terms of maintenance and architecture.

How it works in go: It is often achieved by using structs and interfaces


Bad Example

Most common newbie writing code example… is dumping it all into one function without much structure.

Bad example:

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
	db, _ := sql.Open("postgres", connString)

	name := r.FormValue("name")

	if name == "" {
		http.Error(w, "name required", 400)
		return
	}

	_, err := db.Exec("INSERT INTO users(name) VALUES($1)", name)
	if err != nil {
		http.Error(w, "db error", 500)
		return
	}

	fmt.Fprintln(w, "user created")
}

It might work for this example as it is small. However any real world code would quickly outgrow this structure.

The main reason why this is bad is because everything is intertwined and modifying any part requires knowing all the parts. This is where we should implement separation of concerns.

Proper basic structure

A basic structure should have at least a couple of levels of separation.

One of the most common one is handler/dealing with http, service/business logic, repository/database access, and a model for data structures.

ConcernResponsibility
HTTP LayerHandles request/response
Service LayerHandles business logic
Repository LayerHandles database access
Model LayerHandles data structures

In practice it might look something similar to the code below:

Http layer

	// user/handler.go
	// -> No business logic here — just HTTP concerns.
	type UserHandler struct {
		service *UserService
	}

	type UserService interface {
    	GetUser(id string) (*User, error)
	}

	func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
		id := r.URL.Query().Get("id")

		user, err := h.service.GetUser(id)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		json.NewEncoder(w).Encode(user)
	}

Service layer

	// user/service.go
	// -> No HTTP, no SQL — just logic.
	type UserService struct {
		repo UserRepository
	}

	type UserRepository interface {
    	FindByID(id string) (*User, error)
	}

	func (s *UserService) GetUser(id string) (*User, error) {
		if id == "" {
			return nil, errors.New("invalid id")
		}

		return s.repo.FindByID(id)
	}

Repository layer

	// user/repository.go
	// -> No business rules, no HTTP - just sql
	type UserRepo struct {
		db *sql.DB
	}

	func (r *UserRepo) FindByID(id string) (*User, error) {
		row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)

		var user User
		err := row.Scan(&user.ID, &user.Name)
		return &user, err
	}

Model layer

	// user/model.go
	// -> no http, no sql, no business logic, just the data structure
	type User struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}

The go way

Unlike some other languages and frameworks, like java’s spring or ruby’s rails where it’s all about following framework, go’s philosophy is doing separation of concerns without heavy framework but through a couple of best practices instead. The following pop out:

-> Do small packages. This isn’t about how many lines there are—it’s about having a package with one clear responsibility that’s easy to grasp at a glance.

-> Use interfaces. In go, unlike some other languages, like for example java or c#, you don’t need to manually type:

 class UserRepository implements UserRepositoryInterface

Instead in go, interfaces are automatically inferred at compile time. The official term for writing code that way is structural typing.

So the go way would be similar to the example below. Notice how we never say userRepo implements UserRepository, it is just implied that it does… and that we can use *userRepo wherever UserRepository is required.

	// user/service.go
	type UserService struct {
		repo UserRepository
	}

	func (s *UserService) GetUser(id string) (*User, error) {
		if id == "" {
			return nil, errors.New("invalid id")
		}

		return s.repo.FindByID(id)
	}

	type UserRepository interface {
    	FindByID(id string) (*User, error)
	}

	type userRepo struct {
		db *sql.DB
	}

	func (r *userRepo) FindByID(id string) (*User, error) {
		row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)

		var user User
		err := row.Scan(&user.ID, &user.Name)
		return &user, err
	}

-> composition. Composition with dependency injection is a good way of building code. To expand on our User example. You would do composition in app.go like this:

	// create dependency injection constructors
	
	// user/repository.go
	type userRepo struct {
		db *sql.DB
	}

	func NewUserRepo(db *sql.DB) *userRepo {
		return &userRepo{db:db}
	}

	// user/service.go
	type userService struct {
		repo UserRepository
	}

	type UserRepository interface {
    	FindByID(id string) (*User, error)
	}

	func NewUserService(repo *UserRepository) *userService {
		return &userService{repo:repo}
	}

	// user/handler.go
	type userHandler struct {
		service *UserService
	}

	type UserService interface {
    	GetUser(id string) (*User, error)
	}

	func NewUserHandler(service UserService) *userHandler {
		return &userHandler(service: service)
	}


	// user/app.go
	// -> compose the user app
	func main() {
		db := setupDB()

		repo := &userRepo{db: db}
		service := NewUserService(repo)
		handler := NewUserHandler(service)

		http.HandleFunc("/user", handler.GetUser)
	}

Interfaces tips and tricks

One neat trick about interfaces and code navigation is that you can select the interface and right click it and click “find all implementations” and you will see all the structs that implement the given interface. Most of code editors support that feature. However some might have different name for it.

Another neat trick about code navigation is that you can right click the identifier, for example user.Model and click find all references, which will show you all usages of Model.

Another neat trick is when you want to say some struct must implement interface. You can write something like this:

var _ MyInterface = (*MyStruct)(nil)

Summary

We’ve learned a bit how to structure your code, the good and the bad way. We’ve learned what are specific common practices to have separation of concerns when using go. And in the end we have some tips and tricks how to navigate code and how to tell the compiler that interface must be implemented by a certain struct.