Layered Design
lastmod 2025-04-25created 2025-04-253 edits
A design method for preventing circular references between packages.
About
- Because Go strictly forbids circular references between packages, it naturally leads to a layered design
- This article explains the layered structure that Go projects are obliged to have, and argues that it is effective enough on its own without imposing a separate architecture on top
- When a circular dependency occurs, it presents concrete, practical refactoring strategies step by step to resolve it
- Each package is designed to be a meaningful functional unit on its own, which also benefits testing, maintenance, and splitting into microservices
- As a result, this approach prevents the common real-world code design problem of "you wanted a banana but got the whole jungle"
The layered design approach in Go
Basic principles
- Go forbids circular references between packages
- The import relationships of every Go program must form a directed acyclic graph (DAG)
- This structure is not optional but a design rule enforced at the language level
Automatic formation of package layers
- Excluding external packages, the project's internal packages can be layered automatically by reference depth
- As in the figure below, core utility packages such as metrics, logging, and shared data structures sit at the very bottom
- Higher-level packages then stack on top, progressively combining functionality
Characteristics of this design approach
- Layers are based on the direction of references, not on hierarchical abstraction
- A single package can reference many lower-level packages
- Existing design approaches such as MVC and hexagonal architecture can also be "applied" on top of this structure
- However, Go's structural constraints must be taken into account
Strategies for resolving circular references
When a circular reference occurs, try refactoring in the following order:
- Move the functionality
- The most recommended approach
- Analyze exactly which functionality causes the cycle and move it to a logically appropriate place
- Not used often, but improves conceptual clarity the most
- Split shared functionality into a separate package
- Move types or functions used by both sides (Username, etc.) into a third package
- Split boldly even if the package looks small
- That package is likely to grow over time
- Create a higher-level composing package
- Create a third package that combines the two circular packages
- Example: separate the bidirectional dependency between Category and BlogPost into a higher-level package
- Keep the lower-level packages as dumb structs and compose the actual functionality in the higher-level package
- Introduce interfaces
- Replace the dependency on a struct or function with an interface that has only the methods needed
- Removes unnecessary dependencies and makes testing easier
- However, overuse can actually make the design more complex
- Copy
- If the dependency is very small, simply copy it
- It may look like a DRY violation, but in practice it often helps clarify the design
- Merge into one package
- If none of the above is possible, merge the two packages
- Acceptable as long as it doesn't become too large a package
- However, avoid merging unconditionally and decide carefully
Practical advantages of this design approach
- Each package is a meaningful functional unit on its own and can be tested independently
- References within packages are limited, so individual packages can be understood without understanding the whole codebase
- Avoids unintended chaining of all dependencies (= the jungle problem) and encourages writing code that uses only what it needs
- Easy to extract when splitting into microservices
- Most dependencies are clearly defined
Conclusion
- Go's package design constraints are not an annoying restriction but a device that leads to good design
- A robust design is possible with just the reference structure between packages, without any special architecture
- Careful analysis of circular references and refactoring strategies are valid not only for Go but for other languages as well
See also
Favorite site
Pages that link here