- Use `internal/` for private business logic - Define interfaces where you use them, not where you implement - Keep import graph flat and unidirectional - Organize by features, not layers (user/, not controllers/) - Start with a monolith, split when needed - No utils/helpers/common packages - be specific - One package = one clear responsibility- Use `internal/` for private business logic - Define interfaces where you use them, not where you implement - Keep import graph flat and unidirectional - Organize by features, not layers (user/, not controllers/) - Start with a monolith, split when needed - No utils/helpers/common packages - be specific - One package = one clear responsibility

Clean Code in Go (Part 4): Package Architecture, Dependency Flow, and Scalability

2025/11/28 04:12
9 min read

This is the fourth article in the "Clean Code in Go" series.

Previous Parts:

  • Clean Code: Functions and Error Handling in Go: From Chaos to Clarity [Part 1]
  • Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance
  • Clean Code: Interfaces in Go - Why Small Is Beautiful [Part 3]

Why Import Cycles Hurt

I've spent countless hours helping teams untangle circular dependencies in their Go projects. "Can't load package: import cycle not allowed" — if you've seen this error, you know how painful it is to refactor tangled dependencies. Go is merciless: no circular imports, period. And this isn't a bug, it's a feature that forces you to think about architecture. \n \n Common package organization mistakes I've seen: \n - Circular dependencies attempted: ~35% of large Go projects \n - Everything in one package: ~25% of small projects \n - Utils/helpers/common packages: ~60% of codebases \n - Wrong interface placement: ~70% of packages \n - Over-engineering with micropackages: ~30% of projects

After 6 years working with Go and reviewing architecture in projects from startups to enterprise, I've seen projects with perfect package structure and projects where everything imports everything (spoiler: the latter don't live long). Today we'll explore how to organize packages so your project scales without pain and new developers understand the structure at first glance.

Anatomy of a Good Package

Package Name = Purpose

// BAD: generic names say nothing package utils package helpers package common package shared package lib // GOOD: name describes purpose package auth // authentication and authorization package storage // storage operations package validator // data validation package mailer // email sending

Project Structure: Flat vs Nested

BAD: Java-style deep nesting /src /main /java /com /company /project /controllers /services /repositories /models # GOOD: Go flat structure /cmd /api # API server entry point /worker # worker entry point /internal # private code /auth # authentication /storage # storage layer /transport # HTTP/gRPC handlers /pkg # public packages /logger # reusable /crypto # crypto utilities

Internal: Private Project Packages

Go 1.4+ has a special `internal` directory whose code is accessible only to the parent package:

\

// Structure: // myproject/ // cmd/api/main.go // internal/ // auth/auth.go // storage/storage.go // pkg/ // client/client.go // cmd/api/main.go - CAN import internal import "myproject/internal/auth" // pkg/client/client.go - CANNOT import internal import "myproject/internal/auth" // compilation error! // Another project - CANNOT import internal import "github.com/you/myproject/internal/auth" // compilation error!

Rule: internal for Business Logic

// internal/user/service.go - business logic is hidden package user type Service struct { repo Repository mail Mailer } func NewService(repo Repository, mail Mailer) *Service { return &Service{repo: repo, mail: mail} } func (s *Service) Register(email, password string) (*User, error) { // validation if err := validateEmail(email); err != nil { return nil, fmt.Errorf("invalid email: %w", err) } // check existence if exists, _ := s.repo.EmailExists(email); exists { return nil, ErrEmailTaken } // create user user := &User{ Email: email, Password: hashPassword(password), } if err := s.repo.Save(user); err != nil { return nil, fmt.Errorf("save user: %w", err) } // send welcome email s.mail.SendWelcome(user.Email) return user, nil }

Dependency Inversion: Interfaces on Consumer Side

Rule: Define Interfaces Where You Use Them

// BAD: interface in implementation package // storage/interface.go package storage type Storage interface { Save(key string, data []byte) error Load(key string) ([]byte, error) } // storage/redis.go type RedisStorage struct { client *redis.Client } func (r *RedisStorage) Save(key string, data []byte) error { /*...*/ } func (r *RedisStorage) Load(key string) ([]byte, error) { /*...*/ } // PROBLEM: service depends on storage // service/user.go package service import "myapp/storage" // dependency on concrete package! type UserService struct { store storage.Storage }

\

// GOOD: interface in usage package // service/user.go package service // Interface defined where it's used type Storage interface { Save(key string, data []byte) error Load(key string) ([]byte, error) } type UserService struct { store Storage // using local interface } // storage/redis.go package storage // RedisStorage automatically satisfies service.Storage type RedisStorage struct { client *redis.Client } func (r *RedisStorage) Save(key string, data []byte) error { /*...*/ } func (r *RedisStorage) Load(key string) ([]byte, error) { /*...*/ } // main.go package main import ( "myapp/service" "myapp/storage" ) func main() { store := storage.NewRedisStorage() svc := service.NewUserService(store) // storage satisfies service.Storage }

Import Graph: Wide and Flat

Problem: Spaghetti Dependencies

// BAD: everyone imports everyone // models imports utils // utils imports config // config imports models // CYCLE! // controllers imports services, models, utils // services imports repositories, models, utils // repositories imports models, database, utils // utils imports... everything

Solution: Unidirectional Dependencies

// Application layers (top to bottom) // main // ↓ // transport (HTTP/gRPC handlers) // ↓ // service (business logic) // ↓ // repository (data access) // ↓ // models (data structures) // models/user.go - zero dependencies package models type User struct { ID string Email string Password string } // repository/user.go - depends only on models package repository import "myapp/models" type UserRepository interface { Find(id string) (*models.User, error) Save(user *models.User) error } // service/user.go - depends on models and defines interfaces package service import "myapp/models" type Repository interface { Find(id string) (*models.User, error) Save(user *models.User) error } type Service struct { repo Repository } // transport/http.go - depends on service and models package transport import ( "myapp/models" "myapp/service" ) type Handler struct { svc *service.Service }

Organization: By Feature vs By Layer

By Layers (Traditional MVC)

project/ /controllers user_controller.go post_controller.go comment_controller.go /services user_service.go post_service.go comment_service.go /repositories user_repository.go post_repository.go comment_repository.go /models user.go post.go comment.go # Problem: changing User requires edits in 4 places

By Features (Domain-Driven)

project/ /user handler.go # HTTP handlers service.go # business logic repository.go # database operations user.go # model /post handler.go service.go repository.go post.go /comment handler.go service.go repository.go comment.go # Advantage: all User logic in one place

Hybrid Approach

project/ /cmd /api main.go /internal /user # user feature service.go repository.go /post # post feature service.go repository.go /auth # auth feature jwt.go middleware.go /transport # shared transport layer /http server.go router.go /grpc server.go /storage # shared storage layer postgres.go redis.go /pkg /logger /validator

Dependency Management: go.mod

Minimal Version Selection (MVS)

// go.mod module github.com/yourname/project go 1.21 require ( github.com/gorilla/mux v1.8.0 github.com/lib/pq v1.10.0 github.com/redis/go-redis/v9 v9.0.0 ) // Use specific versions, not latest // BAD: // go get github.com/some/package@latest // GOOD: // go get github.com/some/[email protected]

Replace for Local Development

// go.mod for local development replace github.com/yourname/shared => ../shared // For different environments replace github.com/company/internal-lib => ( github.com/company/internal-lib v1.0.0 // production ../internal-lib // development )

Code Organization Patterns

Pattern: Options in Separate File

package/ server.go # main logic options.go # configuration options middleware.go # middleware errors.go # custom errors doc.go # package documentation

\

// options.go package server type Option func(*Server) func WithPort(port int) Option { return func(s *Server) { s.port = port } } func WithTimeout(timeout time.Duration) Option { return func(s *Server) { s.timeout = timeout } } // errors.go package server import "errors" var ( ErrServerStopped = errors.New("server stopped") ErrInvalidPort = errors.New("invalid port") ) // doc.go // Package server provides HTTP server implementation. // // Usage: // srv := server.New( // server.WithPort(8080), // server.WithTimeout(30*time.Second), // ) package server

Pattern: Facade for Complex Packages

// crypto/facade.go - simple API for complex package package crypto // Simple functions for 90% of use cases func Encrypt(data, password []byte) ([]byte, error) { return defaultCipher.Encrypt(data, password) } func Decrypt(data, password []byte) ([]byte, error) { return defaultCipher.Decrypt(data, password) } // For advanced cases - full access type Cipher struct { algorithm Algorithm mode Mode padding Padding } func NewCipher(opts ...Option) *Cipher { // configuration }

Testing and Packages

Test Packages for Black Box Testing

// user.go package user type User struct { Name string age int // private field } // user_test.go - white box (access to private fields) package user func TestUserAge(t *testing.T) { u := User{age: 25} // access to private field // testing } // user_blackbox_test.go - black box package user_test // separate package! import ( "testing" "myapp/user" ) func TestUser(t *testing.T) { u := user.New("John") // only public API // testing }

Anti-patterns and How to Avoid Them

Anti-pattern: Models Package for Everything

// BAD: all models in one package package models type User struct {} type Post struct {} type Comment struct {} type Order struct {} type Payment struct {} // 100500 structs... // BETTER: group by domain package user type User struct {} package billing type Order struct {} type Payment struct {}

Anti-pattern: Leaking Implementation Details

// BAD: package exposes technology package mysql type MySQLUserRepository struct {} // BETTER: hide details package storage type UserRepository struct { db *sql.DB // details hidden inside }

Practical Tips

1. Start with a monolith— don't split into micropackages immediately \n 2.internal for all private code— protection from external dependencies \n 3.Define interfaces at consumer— not at implementation \n 4.Group by features, not by file types \n 5. **One package = one responsibility \ 6. Avoid circular dependenciesthrough interfaces \n 7.Document packages in doc.go

Package Organization Checklist

- Package has clear, specific name \n - No circular imports \n - Private code in internal \n - Interfaces defined at usage site \n - Import graph flows top to bottom \n - Package solves one problem \n - Has doc.go with examples \n - Tests in separate test package

Conclusion

Proper package organization is the foundation of a scalable Go project. Flat import graph, clear responsibility boundaries, and Dependency Inversion through interfaces allow project growth without the pain of circular dependencies. \n \n In the final article of the series, we'll discuss concurrency and context — unique Go features that make the language perfect for modern distributed systems. \n \n What's your approach to package organization? Do you prefer organizing by feature or by layer? How do you handle the temptation to create a "utils" package? Let me know in the comments!

\

Market Opportunity
Particl Logo
Particl Price(PART)
$0.2613
$0.2613$0.2613
-0.22%
USD
Particl (PART) Live Price Chart
Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact [email protected] for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

Young Republicans were more proud to be American under Obama than under Trump: data analyst

Young Republicans were more proud to be American under Obama than under Trump: data analyst

CNN data analyst Harry Enten sorts through revealing polls and surveys of American attitudes, looking for shifts, and his latest finding is an indictment of President
Share
Alternet2026/02/10 22:18
Vitalik Buterin Outlines Ethereum’s AI Framework, Pushes Back Against Solana’s Acceleration Thesis

Vitalik Buterin Outlines Ethereum’s AI Framework, Pushes Back Against Solana’s Acceleration Thesis

Ethereum co-founder Vitalik Buterin has reacted to Solana’s artificial general intelligence acceleration initiative. He did this through the establishment of his
Share
Thenewscrypto2026/02/10 18:40
Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple!

Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple!

Buterin unveils Ethereum’s strategy to tackle quantum security challenges ahead. Ethereum focuses on simplifying architecture while boosting security for users. Ethereum’s market stability grows as Buterin’s roadmap gains investor confidence. Ethereum founder Vitalik Buterin has unveiled his long-term vision for the blockchain, focusing on making Ethereum quantum-secure while maintaining its simplicity for users. Buterin presented his roadmap at the Japanese Developer Conference, and splits the future of Ethereum into three phases: short-term, mid-term, and long-term. Buterin’s most ambitious goal for Ethereum is to safeguard the blockchain against the threats posed by quantum computing.  The danger of such future developments is that the future may call into question the cryptographic security of most blockchain systems, and Ethereum will be able to remain ahead thanks to more sophisticated mathematical techniques to ensure the safety and integrity of its protocols. Buterin is committed to ensuring that Ethereum evolves in a way that not only meets today’s security challenges but also prepares for the unknowns of tomorrow. Also Read: Ethereum Giant The Ether Machine Takes Major Step Toward Going Public! However, in spite of such high ambitions, Buterin insisted that Ethereum also needed to simplify its architecture. An important aspect of this vision is to remove unnecessary complexity and make Ethereum more accessible and maintainable without losing its strong security capabilities. Security and simplicity form the core of Buterin’s strategy, as they guarantee that the users of Ethereum experience both security and smooth processes. Focus on Speed and Efficiency in the Short-Term In the short term, Buterin aims to enhance Ethereum’s transaction efficiency, a crucial step toward improving scalability and reducing transaction costs. These advantages are attributed to the fact that, within the mid-term, Ethereum is planning to enhance the speed of transactions in layer-2 networks. According to Butterin, this is part of Ethereum’s expansion, particularly because there is still more need to use blockchain technology to date. The other important aspect of Ethereum’s development is the layer-2 solutions. Buterin supports an approach in which the layer-2 networks are dependent on layer-1 to perform some essential tasks like data security, proof, and censorship resistance. This will enable the layer-2 systems of Ethereum to be concerned with verifying and sequencing transactions, which will improve the overall speed and efficiency of the network. Ethereum’s Market Stability Reflects Confidence in Long-Term Strategy Ethereum’s market performance has remained solid, with the cryptocurrency holding steady above $4,000. Currently priced at $4,492.15, Ethereum has experienced a slight 0.93% increase over the last 24 hours, while its trading volume surged by 8.72%, reaching $34.14 billion. These figures point to growing investor confidence in Ethereum’s long-term vision. The crypto community remains optimistic about Ethereum’s future, with many predicting the price could rise to $5,500 by mid-October. Buterin’s clear, forward-thinking strategy continues to build trust in Ethereum as one of the most secure and scalable blockchain platforms in the market. Also Read: Whales Dump 200 Million XRP in Just 2 Weeks – Is XRP’s Price on the Verge of Collapse? The post Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple! appeared first on 36Crypto.
Share
Coinstats2025/09/18 01:22