Layered Architecture in Go: Structs and Interfaces in Action


September 16, 2025 Program

Layered Architecture in Go: Structs and Interfaces in Action


A practical guide to implementing layered architecture in Go, showing how structs and interfaces work together to create modular, testable, and maintainable applications.

Before diving into layered architecture, make sure you’re already comfortable with Go basics — especially structs, methods, and interfaces. If you haven’t yet, check out the previous tutorial first: Understanding Structs and Interfaces in Go .

In this post, we’ll take those building blocks and show how they’re used in real-world applications, where code needs to be modular, testable, and maintainable.

Start here if you’re new↓

Understanding Structs and Interfaces in Go


In real-world Go applications, we rarely use structs and interfaces alone. Instead, we combine them to build layered architectures that are modular, testable, and maintainable.

Why Layered Architecture?

🔗

Separation of concerns:

  • Handler → Talks to outside world (HTTP, gRPC, CLI)
  • Service → Contains business logic
  • Repository → Defines how we access data (via interface)
  • Database → Actual storage (Postgres, MySQL, MongoDB, or even memory)

Architecture

🔗
Diagram
Handler (API / UI)
        ↓
Service (Business logic)
        ↓
Repository (Data access)
        ↓
Database (Storage)

Example in Go

🔗

Repository Layer

🔗
go
type UserRepository interface {
    Create(user *User) error
    GetByEmail(email string) (*User, error)
}

Service Layer

🔗
go
type UserService struct {
    repo UserRepository
}

func (s *UserService) RegisterUser(name, email string) error {
    existing, _ := s.repo.GetByEmail(email)
    if existing != nil {
        return fmt.Errorf("user already exists")
    }
    return s.repo.Create(&User{Name: name, Email: email})
}

Handler Layer

🔗
go
func RegisterUserHandler(c *gin.Context) {
    var req struct {
        Name  string `json:"name" binding:"required"`
        Email string `json:"email" binding:"required,email"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    if err := service.RegisterUser(req.Name, req.Email); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.Status(201)
}

Benefits

🔗
  • Testability: Mock repositories easily.
  • Flexibility: Swap out Postgres for Mongo without touching service logic.
  • Clarity: Each layer has one responsibility.

Conclusion

🔗

By combining structs (implementations) and interfaces (contracts), Go developers can build scalable architectures. This layered approach is widely used in the industry and makes your codebase clean and future-proof.

Go



Avatar

Alvin

Software engineer, interested in financial knowledge, health concepts, psychology, independent travel, and system design.

Related Posts