Software architecture has always been about managing complexity for humans. But today, we have a new consumer of our code: Artificial Intelligence.

When we ask an AI agent to "refactor the payment service," and we hand it a 50,000-line monolith, it hallucinates. The context window is flooded with noise, irrelevant imports, and leaky abstractions. The Agent gets lost in the spaghetti.

Hexagonal Architecture (Ports and Adapters), pioneered by Alistair Cockburn, is the solution. It solves the "Context Saturation" problem by enforcing strict boundaries.

The Interface is the Prompt

In a Hexagonal system, the Port (Interface) defines the "What." The Adapter (Implementation) defines the "How."

This separation allows us to treat the AI as a junior developer with a very specific task. We don't say "Build a user service." We say "Implement this Interface using pgx/v5."

The Parnas Principle (1972) Revisited

David Parnas introduced Information Hiding to reduce the "cognitive load" on developers. Today, this applies to the "context load" of LLMs.

Detailed research suggests that modular code significantly reduces AI hallucination rates. By providing an Agent with only the Interface and the Domain/Entity definition, we create a Bounded Context that fits perfectly into the reasoning window.

Step 1: The Core Domain (Pure Go)

The Core Domain contains your business rules. This is where you, the Senior Engineer, spend your time. It has zero dependencies on the outside world. No SQL, no HTTP, no JSON tags (ideally).

package domain

// User is a pure business entity.
// It knows nothing about Postgres or JSON.
type User struct {
  ID    uuid.UUID
  Email string
  Role  UserRole
}

// Ensure business rules are enforced here, not in the DB.
func NewUser(email string) (*User, error) {
  if !isValidEmail(email) {
      return nil, ErrInvalidEmail
  }
  return &User{
      ID:    uuid.New(),
      Email: email,
      Role:  RoleStandard,
  }, nil
}

The Core Domain. Pure logic. 0 external imports.

Step 2: The Port (The Contract)

We define how the outside world interacts with our domain. This is the contract we will hand to the AI Agent.

package ports

import (
  "context"
  "myproject/domain"
)

// Primary Port (Driving)
type UserService interface {
  Register(ctx context.Context, email string) error
}

// Secondary Port (Driven)
// This is the prompt for the AI: "Implement this interface."
type UserRepository interface {
  Save(ctx context.Context, user domain.User) error
  Find(ctx context.Context, email string) (domain.User, error)
}

Step 3: The Adapter (Auto-Generated)

Now we use the AI. We give it the `UserRepository` interface and the `User` struct, and we ask it to generate a Postgres adapter.

Because the boundaries are so clear, the AI executes this perfectly. It handles the boring parts: SQL queries, error mapping, and transaction management.

// Generated by AI Agent
package adapters

type PostgresRepo struct {
  db *pgxpool.Pool
}

func (r *PostgresRepo) Save(ctx context.Context, u domain.User) error {
  const q = `INSERT INTO users (id, email, role) VALUES ($1, $2, $3)`
  _, err := r.db.Exec(ctx, q, u.ID, u.Email, u.Role)
  if err != nil {
      return fmt.Errorf("db: insert user: %w", err)
  }
  return nil
}

The AI handles the boilerplate. You own the architectural boundary.

Step 4: Dependency Injection

Finally, we wire it all together in `main.go`. This is where the concrete implementation is injected into the core service.

func main() {
  // 1. Init Infrastructure (Adapter)
  dbPool, _ := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
  userRepo := adapters.NewPostgresRepo(dbPool)

  // 2. Init Core Service (Domain Logic)
  // The service only knows about the 'UserRepository' interface.
  userService := app.NewUserService(userRepo) 

  // 3. Init Handlers (Driving Adapter)
  server := http.NewServer(userService)
  server.Listen(":8080")
}

Composing the application from modular blocks.

Speed Without Sacrifice

This approach allows us to move at the speed of AI generation without sacrificing quality. If the AI generates a suboptimal adapter, we just regenerate it. The Core Domain remains untouched.

We can swap out an entire database layer or switch from REST to gRPC in minutes because the contract ensures the business logic doesn't care. It is the ultimate form of maintainability in the age of infinite code.