Skip to content
Search

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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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

author Zer0 sitemap