5 ways to solve import cycles in Go.
Go import cycles mean package A imports package B and package B imports package A, such as when Customer has []Order and Order has Customer.
Go blocks this on purpose as:
- The compiler builds one package at a time and it must start from packages that import nothing. A cycle breaks that order.
- Another big reason is startup order. Global variables and init functions run in import order. With a cycle there is no clear order, so code can read a global value before it is ready.
The most useful idea here is to treat an import cycle as a design smell. You are coupling 2 features together, if you want to move a feature to another project, you also need to move another one.
So here are 5 ways to fix them. Assume that:
- package customer: object Customer contains []Order
- package order: object Order contains Customer
--- 1. using IDs instead of full objects
This is the most common design in real systems. Instead of storing a whole Customer inside Order, you only store CustomerID. Instead of storing a list of Order objects inside Customer, you store OrderIDs or nothing at all.
```
package customer
type Customer struct {
ID int64
Name string
OrderIDs []int64
}
---
package order
type Order struct {
ID int64
Product string
CustomerID int64
}
```
When the program needs the real data, another layer loads it. For example, storage code sees CustomerID and fetches the Customer from the database.
--- 2. uses small interfaces
Instead of holding a concrete Customer type inside Order, the order package defines a tiny interface that describes what it needs from a customer.
The order package does not import customer package. Any type that satisfies the interface can be used.
```
package order
type Customer interface {
Pay() int64
Name() string
}
type Order struct {
ID int64
Product string
Customer Customer
}
```
Now Customer implements the interface implicitly. order package does not know about the concrete Customer type. A higher level package wires them together.
--- 3. uses a wrapper package for combined views
Keep Customer and Order clean and independent. Then create a third package that imports both and builds a combined struct used for specific screens or API responses.
```
package customer
type Customer struct {
ID int64
Name string
}
package order
type Order struct {
ID int64
Product string
CustomerID int64
}
package customerorder
type CustomerWithOrders struct {
customer.Customer
Orders []order.Order
}
```
This type is not a core domain object. It is a view model, a shape convenient for one response or one page. You load data from storage and assemble it.
This pattern is very common in APIs and UI backends. The limitation is that the relationship lives only in this wrapper, not inside the base structs.
--- 4. uses one bounded model package
Put both structs in the same package. Because they share the same namespace, they can reference each other directly without imports.
This is the only way to truly have Customer contain []Order and Order contain *Customer as real fields at the same time:
```
package cusordermodel
type Customer struct {
ID int64
Name string
Orders []*Order
}
type Order struct {
ID int64
Product string
Customer *Customer
}
```
Other layers import model, but model imports nothing. This keeps dependencies pointing inward. It is clean when Customer and Order are tightly coupled and always evolve together.
Note: the model package does not mean a global model. It is a local model package only for Customer and Order.
The tradeoff is that the model package can grow large if you put too many types in it.
--- 5. this is a design issue, fix it
Most of the time you do not need both directions. Pick one owner. Usually Order knows Customer, but Customer does not need to store all orders.
If you need a customer with orders, query orders by CustomerID. This keeps the model simple and avoids heavy memory use. Bidirectional links often cause problems like large object graphs and harder updates.
If you put everything inside Customer, things like []Order, []Address, []PaymentMethod, []Review, []Notification, the struct can grow very large over time. It becomes a "god object". Every new feature wants to attach something to Customer because it feels natural. After a while the type is heavy, hard to understand, and expensive to load into memory.
---
In large production systems, 1 and 5 are the most common. 4 appears when types are tightly coupled within the same domain. 2 and 3 are situational tools used for decoupling or building response shapes.