Перейти к основному содержимому

Layered Design

The orchestrator follows a strict top-down layered architecture.

In plain language, this means each part of the backend has one job, and code should pass work downward instead of reaching across layers and doing everything at once.

This is one of the most important patterns in the codebase because it keeps the code maintainable as it grows and makes it easy to test each layer in isolation.

The Four Layers

Layered Architecture
Click diagram to zoom

Each layer has a single responsibility, and dependencies only flow downward. A handler never imports a repo. A repo never references a service. This rule is absolute.

If you are new, the easiest way to read this is:

  • server/ handles HTTP concerns
  • service/ decides business behavior
  • repo/ talks to the database

Why Layers Matter

Without layers, you end up with HTTP handlers that contain SQL queries, business rules, and response formatting all mixed together. That kind of code is hard to test, hard to refactor, and prone to bugs when you change one thing and accidentally break another.

With layers:

  • You can unit test services by mocking repos (no database needed)
  • You can swap the database without touching business logic
  • You can add a gRPC server alongside HTTP by reusing the same service layer
  • New developers can orient quickly because each file has a predictable scope

Layer Rules

LayerKnows AboutDoes NOT Know About
server/Services, request/response DTOs, HTTP concernsDatabase, SQL, repos
service/Repos, domain types, runtime clientHTTP, request parsing, response format
repo/Database, SQL, row typesServices, HTTP, business rules

Think of it this way: if you are writing code in server/, you should never see the word "SQL." If you are writing code in repo/, you should never see http.Request.

The Domain Contract: types.go

Every layer needs to agree on what a "User" or a "Workspace" looks like. That shared vocabulary lives in internal/orchestrator/types.go — the central contract file. It defines:

  • Domain types like Principal, User, Workspace, and Agent
  • Interfaces like IdentityVerifier
  • Constants shared across all layers

When you add a new domain concept, start here. Define the type in types.go, then work your way down: migration, repo, service, handler.

Each sub-package also has its own types.go for layer-specific contracts:

  • repo/types.go — repository interfaces and database row types
  • service/types.go — service interfaces and option structs

Patterns You Will See Everywhere

Session-per-Request

Every HTTP request creates one *dbr.Session (a database connection). That session flows from handler to service to repo. For operations that need atomicity (creating a user and their workspace together), wrap the call with database.WithTransaction[T]().

Typed Option Structs

Service methods accept a single options struct instead of positional arguments. This keeps method signatures stable as new fields are added:

type CreateUserOpts struct {
Subject string
Email string
Name string
}

Adding a Phone field later does not break existing callers.

Structured Errors

Functions return *errors.Error, not the built-in Go error. Every error is either a business error (safe to return to clients, with a code like AUTH0001) or a server error (logged internally, client sees a generic "internal server error"). This forces you to think about error visibility at every call site.

How a Request Flows Through the Layers

Here is a concrete example — what happens when a user signs in:

1
Step 1

server/ receives the request

The handler parses the HTTP body, extracts the principal from auth middleware, and calls authService.SignIn(ctx, opts).

2
Step 2

service/ decides business behavior

The auth service validates the principal, looks up the user through the repo, creates a new user if needed, and returns the user object.

3
Step 3

repo/ talks to PostgreSQL

The user repo runs a SELECT query and returns a row type.

4
Step 4

server/ sends the response

The handler maps the user to a response DTO and writes JSON back to the client.

Each layer does its job and passes the result to the next. No layer reaches past its neighbor.

What's Next

Now that you understand the internal design, see Codebase Layout to orient yourself in the repository structure.