Fix 'import cycle not allowed' in Go: Restructuring Packages to Break Circular Dependencies

intermediate๐Ÿ”ท Go2026-05-13| Go 1.18+, any OS (Linux, macOS, Windows), any project size

Error Message

import cycle not allowed
#go#import-cycle#package#architecture

TL;DR

Go won't compile when two or more packages import each other โ€” directly or through a chain. The fix is always structural: pull shared code into a third package that neither conflicting package touches, or merge the packages if they're too tangled to live apart.

What the error looks like

$ go build ./...
package myapp/service
        imports myapp/repository
        imports myapp/service: import cycle not allowed

Go prints the full import chain so you can pinpoint where the cycle closes. Read it bottom-up: the last package listed completes the circle by importing the first one.

Why Go forbids import cycles

Go compiles packages in strict dependency order. If package A needs package B and B needs A, the compiler hits a deadlock at the type-checking stage โ€” there's no valid compilation order.

Languages like Java or Python sidestep this at link time. Go doesn't. It's a hard compile-time error with no language-level escape hatch. You fix the structure, full stop.

Detecting the cycle

The error message already shows the chain. For larger projects with tangled graphs, a few extra tools help:

# Show all dependencies of a package
go list -f '{{ .Imports }}' ./internal/service

# Find all cycles across the module
go build ./... 2>&1 | grep 'import cycle'

# Generate a visual dependency graph (requires Graphviz)
go install github.com/kisielk/godepgraph@latest
godepgraph -s ./... | dot -Tpng -o deps.png

Common scenarios and fixes

Scenario 1: Service and repository importing each other

Nine out of ten Go cycle errors look like this. The service layer calls the repository โ€” fine. The repository then references a type defined in the service package โ€” not fine.

// BAD: myapp/repository/user.go
import "myapp/service" // cycle!

func (r *UserRepo) Save(u service.User) error { ... }

Fix: Move shared types like User into a dedicated models or domain package. Neither service nor repository imports the other โ€” both import models.

// myapp/models/user.go โ€” leaf package, no internal imports
package models

type User struct {
    ID    int
    Email string
}

// myapp/repository/user.go
import "myapp/models" // OK

func (r *UserRepo) Save(u models.User) error { ... }

// myapp/service/user.go
import (
    "myapp/models"     // OK
    "myapp/repository" // OK
)

func CreateUser(repo *repository.UserRepo, email string) error {
    u := models.User{Email: email}
    return repo.Save(u)
}

Scenario 2: A utility package that reaches into application logic

Someone adds a helper to utils that needs a type from config. Works fine โ€” until config imports utils for something else.

// myapp/utils/helpers.go
import "myapp/config" // cycle if config imports utils

Fix: Utility packages should have zero internal imports. Pass values in as parameters instead of reaching out to the package that owns them.

// BEFORE โ€” helper pulls config itself
func GetTimeout() time.Duration {
    cfg := config.Load()
    return cfg.Timeout
}

// AFTER โ€” caller provides the value
func FormatDuration(d time.Duration) string {
    return d.String()
}

Scenario 3: Two packages that should be one

Sometimes the cycle is a symptom, not the problem. If packageA and packageB call each other constantly and there's no clean extraction point, they belong in the same package.

# Before โ€” two packages that can't live without each other
myapp/auth/
myapp/session/

# After โ€” merge into one cohesive package
myapp/auth/          # contains both auth and session logic

Scenario 4: Using an interface to invert the dependency

When you genuinely need bidirectional communication, define an interface in the lower-level package. The upper-level package implements it. No shared import needed โ€” the dependency flows one way.

// myapp/repository/notifier.go
package repository

// Notifier is implemented by the service layer.
// repository never imports service.
type Notifier interface {
    OnUserSaved(id int)
}

type UserRepo struct {
    notifier Notifier
}

func NewUserRepo(n Notifier) *UserRepo {
    return &UserRepo{notifier: n}
}

func (r *UserRepo) Save(u User) error {
    // ... save logic ...
    r.notifier.OnUserSaved(u.ID)
    return nil
}

// myapp/service/user.go โ€” implements repository.Notifier
package service

import "myapp/repository"

type UserService struct{}

func (s *UserService) OnUserSaved(id int) {
    // handle post-save notification
}

func SetupRepo(svc *UserService) *repository.UserRepo {
    return repository.NewUserRepo(svc) // service passed as the interface
}

Structural rules that prevent cycles

  • Dependencies flow inward: cmd โ†’ service โ†’ repository โ†’ models. Lower layers never import higher layers. Draw this arrow on a whiteboard before you start โ€” violations become obvious.
  • Shared types live in leaf packages: models, domain, or types โ€” packages with zero internal imports. Every other package can safely import them without risk.
  • Interfaces belong to the consumer: Define an interface in the package that uses it, not the one that implements it. This is idiomatic Go and naturally breaks potential cycles before they form.
  • Watch the internal/common trap: A common package that accumulates unrelated helpers solves cycles mechanically but creates a maintenance mess. Be intentional about what actually belongs there.

Verifying the fix

# Clean exit, no output = cycle is gone
go build ./...

# Make sure nothing broke
go test ./...

# Double-check with vet
go vet ./...

# Optional: regenerate the graph to see the before/after
godepgraph -s ./... | dot -Tpng -o deps-after.png

go build ./... exiting with code 0 and silence is your confirmation. Noisy output means there's still work to do.

Quick checklist when you hit this error

  • Read the full cycle chain โ€” identify which package is the odd one out.
  • Is the import needed for just one type or function? Move that type to a shared leaf package.
  • Do the two packages constantly call each other? Merge them.
  • Can you replace the import with a locally defined interface? Do that.
  • Never import a higher-layer package from a lower-layer package โ€” no exceptions.

Related Error Notes