This tutorial series assumes you are familiar with golang, as we will be using it thorough this article series.
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
To obey the rule in Go:
Define interfaces in the outer layer.
Implement them in the inner layer.
Outer layers define WHAT is needed.
Inner layers define HOW it’s done.
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
Check out the code below:
// 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)
}
// 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/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
}
// 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"`
}
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.
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.