Layered Architecture in Go: Structs and Interfaces in Action

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
🔗DiagramHandler (API / UI) ↓ Service (Business logic) ↓ Repository (Data access) ↓ Database (Storage)
Example in Go
🔗Repository Layer
🔗gotype UserRepository interface {
Create(user *User) error
GetByEmail(email string) (*User, error)
}
Service Layer
🔗gotype 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
🔗gofunc 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.

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