Vjerci.com

A personal blog by software engineer

January, 4-th, 2026

Clean Architecture -> Dependency rule



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


About Dependency rule

The core rule is:

Source code dependencies must point only inward.

What that means in practice is:

Inner layers must NOT depend on outer layers

Outer layers CAN depend on inner layers

It all boils down to defining your dependencies/interfaces at caller or in simpler terms define your interfaces where you need/use them. And implement them where you don’t use them.

I’ve written an article about it before, it goes in depth about why it’s a good thing to define interfaces at caller. You can find the article here


Proper dependency rule

To obey the rule in Go:

Define interfaces in the outer layer.

Implement them in the inner layer.

Mental model to use

Outer layers define WHAT is needed.

Inner layers define HOW it’s done.

Dependency rule example

We will use an example from previous article.

However we will switch things a little bit and instead of structuring project packages by domain(user) we will structure it by service,handlers etc. It’s called layer-based structuring(services, repositories, handlers ect).

The import chart would look something like this import-chart

Check out the code below:

Http layer

	// handlers/user.go
	// it is outer layer to service layer
	type UserHandler struct {
		service *UserService
	}

	// define my dependency at caller/inner layer
	type UserService interface {
    	GetUser(id string) (*User, error)
	}

	// outer can point inwards
	var _ UserService = (*services.UserService)(nil)

	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

	// services/user.go
	// it is inner layer to handler
	// it is outer layer to repository layer
	type UserService struct {
		repo UserRepository
	}

	// define my dependency at caller/inner layer
	type UserRepository interface {
    	FindByID(id string) (*User, error)
	}

	// outer can point inwards
	var _ UserRepository = (*repository.userRepo)(nil)

	// implement handler dependency in outer layer
	func (s *UserService) GetUser(id string) (*User, error) {
		if id == "" {
			return nil, errors.New("invalid id")
		}

		return s.repo.FindByID(id)
	}

Repository layer

	// repository/user.go
	// it is inner layer to service
	type UserRepo struct {
		db *sql.DB
	}

	// implement service dependency in outer layer
	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

	// models/user.go
	// it is inner layer to handler
	// it is inner layer to service
	// it is inner layer to repository

	// can't import anything as it is most inner domain
	type User struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}

Circular dependencies

Some of of the people using other programming languages before hopping into golang might be surprised to find out go is a language that doesn’t support circular dependencies between packages.

For example you can do this in javascript and it would work:

	// handler.js
	import { userModel, User } from "./userModel";

	export const userHandler = {
		create(name: string): User {
			return {
				name: name,
				id: uuid.new()
			}
		},

		onUserCreated(user: User) {
			console.log("User created:", user.name);
		}
	};


	// model.js
	import { userHandler } from "./userHandler";

	export interface User {
		id: string;
		name: string;
	}

	export class UserModel {
		private users: User[] = [];
		createUser(name: string): User {
			userHandler.onUserCreated(user);

			return user;
		}
	}

However it is considered a source of confusion and bad practice in golang, so circular dependencies simply don’t work in go.

If you try to do something like this in golang:

	package handler

	import "myapp/user"

	func CreateUser(name string) user.User {
		m := user.Model{}
		return m.CreateUser(name)
	}

	func OnUserCreated(u user.User) {
		log.PrintLn("User created:", u.Name)

		if u.Name == "admin" {
			// ❌ imports model → creates circular dependency
			m := user.Model{}
			m.CreateUser("audit-log")
		}
	}
	package user

	import "myapp/handler"

	type User struct {
		ID   string
		Name string
	}

	type Model struct{}

	func (m *Model) CreateUser(name string) User {
		user := User{
			ID:   "123",
			Name: name,
		}

		// ❌ Calls handler → creates circular dependency
		handler.OnUserCreated(user)

		return user
	}

You get a compile time error

import cycle not allowed
package myapp/user
    imports myapp/handler
    imports myapp/user

A more common error people encounter is when you try to do

package a -> imports b
package b -> imports c
package c -> imports a

Basically golang in its own way softly enforces dependency rule. Without circular dependencies there is just a matter of do you want outer to import inner or inner to import outer.

Even though go doesn’t explicitly enforces it, choosing outer to import inner has tremendous benefits and allows things to fall in place quite nicely.


Summary

We’ve learned about imports in golang, we’ve learned how to structure your code by layer instead of by domain. We’ve also learned how golang softly enforces dependency rule.