background-shape
Layers of Clean Architecture, Domain, Use Case, Adapter, Infra
June 3, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — Four layers: Domain (entities, value objects, domain services) → Use Case (orchestration, ports) → Adapter (HTTP, DB, queue impls) → Infra (framework, drivers, config). Imports flow inward only. Concrete examples below.

After the “why” post, this post is the conceptual map. What lives where, why, and what the dependency rule actually means in code.

I’ll use a small subscription billing service as the running example. Goal: implement “start a monthly subscription” cleanly.

The dependency rule

One rule that defines everything: source code dependencies must point inward.

[Infra] → [Adapter] → [Use Case] → [Domain]

A use-case file CAN import a domain file. A domain file CANNOT import a use-case file. An adapter CAN import use cases; use cases CANNOT import adapters.

In practice this means inner layers know about interfaces, outer layers know about implementations. Use case says “I need something that can save a Subscription.” The Postgres adapter says “I implement that.”

Layer 1: Domain

Lives in: internal/domain/ or app/Domain/

Contains:

  • Entities — types with identity (Subscription, Customer)
  • Value objects — types without identity (Money, BillingPeriod)
  • Domain services — operations that don’t fit on one entity (ProrationCalculator)
  • Domain errors — ErrSubscriptionInactive, etc.

Does NOT contain: Any imports of HTTP, DB, queue, JSON serialization, or framework code.

Example in Go:

package domain

import (
    "time"
    "github.com/google/uuid"
)

type SubscriptionStatus string

const (
    StatusActive   SubscriptionStatus = "active"
    StatusPastDue  SubscriptionStatus = "past_due"
    StatusCanceled SubscriptionStatus = "canceled"
)

type Subscription struct {
    ID         uuid.UUID
    CustomerID uuid.UUID
    PlanID     string
    Status     SubscriptionStatus
    StartedAt  time.Time
    RenewsAt   time.Time
}

func NewSubscription(customerID uuid.UUID, planID string, now time.Time) (*Subscription, error) {
    if planID == "" {
        return nil, ErrInvalidPlan
    }
    return &Subscription{
        ID:         uuid.New(),
        CustomerID: customerID,
        PlanID:     planID,
        Status:     StatusActive,
        StartedAt:  now,
        RenewsAt:   now.AddDate(0, 1, 0),
    }, nil
}

func (s *Subscription) Cancel(now time.Time) error {
    if s.Status == StatusCanceled {
        return ErrAlreadyCanceled
    }
    s.Status = StatusCanceled
    return nil
}

uuid and time are standard-library or pure utilities. No framework imports.

Layer 2: Use Case

Lives in: internal/usecase/ or app/UseCase/

Contains:

  • Use case structs that orchestrate domain operations
  • Ports (interfaces) for things use cases depend on
  • Use case input/output DTOs

Example:

package usecase

import (
    "context"
    "github.com/google/uuid"
    "yourorg/billing/internal/domain"
)

type SubscriptionRepository interface {
    Save(ctx context.Context, s *domain.Subscription) error
    GetByID(ctx context.Context, id uuid.UUID) (*domain.Subscription, error)
}

type EventPublisher interface {
    Publish(ctx context.Context, event domain.Event) error
}

type StartSubscriptionInput struct {
    CustomerID uuid.UUID
    PlanID     string
}

type StartSubscription struct {
    repo  SubscriptionRepository
    pub   EventPublisher
    clock func() time.Time
}

func NewStartSubscription(repo SubscriptionRepository, pub EventPublisher, clock func() time.Time) *StartSubscription {
    return &StartSubscription{repo: repo, pub: pub, clock: clock}
}

func (u *StartSubscription) Execute(ctx context.Context, in StartSubscriptionInput) (*domain.Subscription, error) {
    sub, err := domain.NewSubscription(in.CustomerID, in.PlanID, u.clock())
    if err != nil {
        return nil, err
    }
    if err := u.repo.Save(ctx, sub); err != nil {
        return nil, err
    }
    _ = u.pub.Publish(ctx, domain.SubscriptionStartedEvent{ID: sub.ID})
    return sub, nil
}

Critical: SubscriptionRepository is an interface defined HERE in the use-case layer. The concrete Postgres implementation lives in the adapter layer. The use case has no idea where the data goes.

Layer 3: Adapter

Lives in: internal/adapter/ (or split into internal/http, internal/repo, etc.)

Contains:

  • HTTP handlers that call use cases
  • Postgres / Mongo / Redis repository implementations
  • Queue publishers and consumers
  • External API clients

Example — Postgres implementation of the repository:

package postgres

import (
    "context"
    "github.com/google/uuid"
    "github.com/jackc/pgx/v4/pgxpool"
    "yourorg/billing/internal/domain"
)

type SubscriptionRepository struct {
    db *pgxpool.Pool
}

func NewSubscriptionRepository(db *pgxpool.Pool) *SubscriptionRepository {
    return &SubscriptionRepository{db: db}
}

func (r *SubscriptionRepository) Save(ctx context.Context, s *domain.Subscription) error {
    _, err := r.db.Exec(ctx,
        `INSERT INTO subscriptions (id, customer_id, plan_id, status, started_at, renews_at)
         VALUES ($1, $2, $3, $4, $5, $6)
         ON CONFLICT (id) DO UPDATE SET
           status = EXCLUDED.status,
           renews_at = EXCLUDED.renews_at`,
        s.ID, s.CustomerID, s.PlanID, string(s.Status), s.StartedAt, s.RenewsAt)
    return err
}

func (r *SubscriptionRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Subscription, error) {
    // ... SELECT and map row to *domain.Subscription
}

It imports domain (allowed: inward). It does NOT import usecase. The compile-time check: does it satisfy usecase.SubscriptionRepository? Yes, by structural typing. Wired up at startup.

Layer 4: Infrastructure / Framework

Lives in: cmd/billing/main.go, config/, wire.go

Contains:

  • Framework setup (Gin, chi, Laravel boot)
  • Configuration loading
  • Dependency wiring (DI container or manual wire)
  • Process lifecycle (graceful shutdown, signals)

This is where you mount adapters into use cases and connect use cases to inputs:

func main() {
    cfg := config.Load()
    pool := mustConnectPg(cfg.DBURL)
    defer pool.Close()

    subRepo := postgres.NewSubscriptionRepository(pool)
    pub := kafka.NewPublisher(cfg.KafkaBrokers)

    startSub := usecase.NewStartSubscription(subRepo, pub, time.Now)

    router := chi.NewRouter()
    router.Post("/subscriptions", http.NewSubscriptionHandler(startSub).Create)

    srv := &http.Server{Addr: ":8080", Handler: router}
    log.Fatal(srv.ListenAndServe())
}

main.go is the only file that imports everything. It’s the wiring.

Mapping to Laravel

Laravel maps reasonably:

Clean Laravel
Domain app/Domain/ — plain PHP classes
Use Case app/UseCase/ — also called Services or Actions
Adapter app/Http/Controllers/, app/Repository/, etc.
Infra app/Providers/, framework boot

Eloquent gets awkward — covered in the Eloquent post.

What goes where — quick decisions

  • A function that validates email format → domain (value object)
  • A function that hashes a password → adapter (calls a hash library)
  • A function that calls Stripe → adapter (port defined in use case)
  • A function that decides who gets a refund → domain or use case (depends on complexity)
  • A function that converts JSON → adapter (HTTP-specific)
  • A function that calculates proration → domain (pure business rule)

When in doubt: can this function exist without the framework? Then domain. Does it need a specific library? Then adapter.

Common Pitfalls

Putting validation in the controller. Domain or use case. Validation isn’t framework-specific.

Letting Eloquent / Sequelize / Active Record types leak into use cases. ORM models are adapter-layer. Use cases work with domain types.

No interfaces, calling implementations directly. Use cases coupled to adapters defeats the whole point.

Use cases that return raw rows. Should return domain entities or use-case DTOs. Adapters convert.

Overdoing it: 3-line use cases. If a use case is just “save this entity,” consider whether the controller can do it directly.

Wrapping Up

Four layers, one rule, real code. Monday: the Go project layout that implements all of this without ceremony.