Technology Aug 28, 2026 · 6 min read

Go Doesn't Force Clean Architecture. That's Your Job.

The criticism of this is everywhere. Open any Go thread long enough and someone will show up to perform the same ritual: "Go projects become messy. There's no framework to guide you. Nest, Django, Spring, they all tell you exactly where to put things. Go? It just says 'organize it somehow.'" It's...

DE
DEV Community
by Adam - The Developer ✨
Go Doesn't Force Clean Architecture. That's Your Job.

The criticism of this is everywhere. Open any Go thread long enough and someone will show up to perform the same ritual:

"Go projects become messy. There's no framework to guide you. Nest, Django, Spring, they all tell you exactly where to put things. Go? It just says 'organize it somehow.'"

It's a fair criticism. Go is unusually permissive about structure. I just think blaming Go for a messy codebase is like blaming the empty document for the bad essay.

I don't think Go encourages bad architecture but rather it exposes it.

The Hell Is A Perfect Folder Structure??

Ask a hundred Go developers where to put business logic and you'll get a hundred answers (and 200 opinions).

  • "Should I use internal/?"
  • "Is everything supposed to live under pkg/?"
  • "Should I follow Clean Architecture?"
  • "What about the cmd/ directory?"

We spend so much time debating folder structures as if the arrangement of directories somehow determines code quality. As if renaming utils/ to pkg/shared/ is going to save us. God.

folders don't create architecture. Dependencies do.

You can meticulously organize your project like this:

my-app/
  cmd/main.go
  internal/
    handler/
    service/
    repository/
  pkg/domain/
  pkg/utils/

And still write tightly coupled garbage. Handlers calling repositories directly. Services importing database drivers. Business logic mixed with HTTP concerns. Everything circular.

Beautiful folders, though. Very organized looking on GitHub.

There are better projects I've seen with just 5 packages, they just don't screenshot as well.

Architecture Is About Dependency Direction

The architecture is about making intentional decisions about how code depends on other code.

Have a look at this:

HTTP Handler
    ↓
Business Service
    ↓
Data Repository

This isn't sacred because of folder names. It's valuable because of what it represents:

  • The handler only knows how to translate HTTP
  • The service only knows business rules
  • The repository only knows how to fetch data
  • Each layer depends on the layer below, never upward

That flow is intentional. If you reverse it, everything breaks:

Repository
    ↓
Service
    ↓
Handler

Now the repository needs to know about HTTP? Nice, you've invented a database driver that also speaks REST and I think it's also a cry for help.

This flow works in a three-file project, a monolith, or a microservice with 50 packages. Go does not care how impressive your tree looks in the README.

Interfaces Belong to the Consumer

This is the part where people coming from Java have a small identity crisis.

In languages like Java, interfaces are typically defined alongside the implementation:

// repository package
public interface UserRepository {
    User find(String id);
    void save(User user);
    void delete(String id);
}

public class PostgresUserRepository implements UserRepository {
    // ...
}

This feels natural. For me, it was the default. The repository defines the contract, the implementation fulfills it, everyone goes home happy. Except the consumer, who now depends on an abstraction it didn't ask for, including delete even though it only wanted find. Very generous. Very unhelpful.

Go flips this around:

// service package
type UserFinder interface {
    Find(id string) (*User, error)
}

type UserStorage interface {
    Save(user *User) error
}

type UserService struct {
    finder  UserFinder
    storage UserStorage
}

The consumer defines exactly what it needs. The implementation simply satisfies those interfaces:

type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) Find(id string) (*User, error) {
    // ...
}

func (r *PostgresUserRepository) Save(user *User) error {
    // ...
}

The consumer owns the interface, not the implementation. Don't define an interface for what you provide; define one for what you need.

UserService doesn't care whether its dependencies are backed by Postgres, Redis, a file, or an API. It asked for Find and Save. That's the whole relationship. Very healthy, honestly.

Go Gives You Freedom

Both a feature and a burden.

Terrifying if you like to be told what to do. Freedom if you want to build thoughtfully. A trap if you thought "no framework" meant "no thinking."

The tradeoff is: frameworks prevent bad decisions by restricting your choices, or well, not really; you can still screw things up. The restriction is mostly psychological. Go makes you responsible for your choices, which is less comforting and more honest.

That means your team can't hide behind "the framework made us do it." You can't blame poor architecture on Rails conventions. If your Go project is a mess, it's because your team made it that way. There's no framework to pin it on. That's the whole feature.

Clean Architecture Isn't a Framework

A common misconception I see constantly: someone reads a Clean Architecture blog post, copies the folder tree into their repo, and waits for the cleanliness to arrive. It does not arrive.

cmd/
internal/
  application/
  domain/
  infrastructure/
  entity/
  repository/
  usecase/
pkg/
tests/

Clean Architecture is about keeping business rules independent from implementation details. You can do that in three files:

cmd/main.go
internal/
  service.go
  postgres.go
pkg/models.go

As long as service.go doesn't know about Postgres, business logic doesn't know about HTTP, and concrete implementations are swappable. That's it. You don't get extra architecture points for the folder named usecase.

Simplicity Doesn't Mean Lack of Discipline

Go lets you write less boilerplate but that doesn't mean less discipline. It means the discipline has to come from you, which is annoying, because boilerplate at least felt like progress, I know.

Clear package boundaries

Every package should have a single, defensible purpose. Importing a package should make semantic sense: import "user/service" says something. import "user/pkg1/internal/common/helpers" says you gave up and started a junk drawer.

Minimal public APIs

In Go, a capital letter exports. Think about what you export from each package. If you're exporting everything, you're not making choices. You're just shouting.

Dependency inversion where appropriate

This doesn't mean "use interfaces for everything." Premature abstraction is real. But when you have external dependencies (database, API, file system), invert them. Let the business logic define the interface the dependency must satisfy.

Small interfaces

The best interfaces in Go are two or three methods, max. An interface with ten methods is usually a hint that you're mixing concerns, or that you ported a Java interface and hoped nobody would notice.

Code reviews focused on design

Your standard code review checklist probably has: "tests?" "error handling?" "efficiency?" Add: "Does this dependency flow make sense? Is this the right abstraction?" Design is as important as correctness. Also easier to miss, because the tests still pass while the architecture quietly dies.

Closing

Architecture doesn't live in a programming language. It lives in the decisions engineers make.

Frameworks can enforce consistency. They can't enforce good judgment. Go just gives you fewer guardrails and assumes you'll use them.

Sometimes that pays off spectacularly. Sometimes it leaves you debugging a mess when you should be sleeping.

Either way, it's on you. That's not a bug in the language. That's the deal.

DE
Source

This article was originally published by DEV Community and written by Adam - The Developer ✨.

Read original article on DEV Community
Back to Discover

Reading List