Skip to main content

Architecture

This page explains how Contract works internally. Understanding the architecture helps you make better decisions when building your APIs and troubleshoot issues when they arise.

The Big Picture

At its core, Contract does one simple thing: it takes your plain Go code and makes it accessible via different network protocols. Here’s how the pieces fit together:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     YOUR CODE (Plain Go)                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  package todo                                                  β”‚  β”‚
β”‚  β”‚  type Service struct { ... }                                   β”‚  β”‚
β”‚  β”‚  func (s *Service) Create(...) (*Todo, error)                  β”‚  β”‚
β”‚  β”‚  func (s *Service) List(...) ([]*Todo, error)                  β”‚  β”‚
β”‚  β”‚                                                                β”‚  β”‚
β”‚  β”‚  No HTTP. No JSON. No framework. Just business logic.          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                                   β”‚ contract.Register[todo.API](todo.NewService())
                                   β”‚
                                   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     CONTRACT (The Bridge)                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Service {                                                     β”‚  β”‚
β”‚  β”‚    Name: "todo"                                                β”‚  β”‚
β”‚  β”‚    Methods: [Create, List, Get, Delete]                        β”‚  β”‚
β”‚  β”‚    Types: {Todo, CreateInput, ListOutput, ...}                 β”‚  β”‚
β”‚  β”‚  }                                                             β”‚  β”‚
β”‚  β”‚                                                                β”‚  β”‚
β”‚  β”‚  Each method has an "Invoker" - a fast way to call it.         β”‚  β”‚
β”‚  β”‚  Each type has a "Schema" - a description for documentation.   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                         β”‚                         β”‚
         β–Ό                         β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   REST Handler  β”‚     β”‚ JSON-RPC Handlerβ”‚     β”‚   MCP Handler   β”‚
β”‚                 β”‚     β”‚                 β”‚     β”‚                 β”‚
β”‚ POST /todos     β”‚     β”‚ method: "Create"β”‚     β”‚ tool: "Create"  β”‚
β”‚ GET /todos      β”‚     β”‚ method: "List"  β”‚     β”‚ tool: "List"    β”‚
β”‚ GET /todos/{id} β”‚     β”‚ method: "Get"   β”‚     β”‚ tool: "Get"     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                         β”‚                         β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                                   β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  HTTP Requests  β”‚
                        β”‚  from Clients   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Three Layers Explained

Layer 1: Your Code (The Service)

This is the Go code you write. It contains your business logic with no knowledge of HTTP, JSON, or any protocol:
// todo/service.go
package todo

type Service struct {
    db *sql.DB  // Your dependencies go here
}

func NewService(db *sql.DB) *Service {
    return &Service{db: db}
}

func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    // This is pure business logic
    // No http.Request, no JSON marshaling, no protocol concerns

    todo := &Todo{
        ID:    generateID(),
        Title: in.Title,
    }

    // Save to database
    _, err := s.db.ExecContext(ctx, "INSERT INTO todos...")
    if err != nil {
        return nil, err
    }

    return todo, nil
}
Why this matters: Your code is easy to test (no HTTP mocking needed), easy to understand (just Go functions), and can be reused in different contexts (CLI tools, background jobs, etc.).

Layer 2: The Contract (The Bridge)

When you call contract.Register(), Contract inspects your service and creates a data structure that describes it:
impl := todo.NewService(db)
svc := contract.Register[todo.API](impl)
This registration process:
  1. Discovers methods using Go’s reflection
  2. Parses signatures to understand inputs and outputs
  3. Generates JSON schemas from your Go types
  4. Creates invokers for fast method calling
The result is a Service struct that knows everything about your API:
svc.Name        // "todo"
svc.Methods     // [Create, List, Get, Delete]
svc.Types       // TypeRegistry with JSON schemas

Layer 3: Transports (The Protocols)

Transports are HTTP handlers that speak different protocols. They:
  1. Receive HTTP requests in their specific format
  2. Find the right method in the contract
  3. Call the method using the invoker
  4. Return the response in their specific format
Each transport does this differently:
TransportRequest FormatResponse Format
RESTHTTP verbs + pathsJSON body
JSON-RPCJSON with method nameJSON-RPC envelope
MCPJSON-RPC with tool callsMCP content blocks

How a Request Flows Through the System

Let’s trace a REST request from start to finish:

Step 1: Client Sends Request

curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Buy milk"}'

Step 2: REST Handler Receives It

The REST handler (mounted by rest.Mount()) receives the HTTP request:
Method: POST
Path:   /todos
Body:   {"title": "Buy milk"}

Step 3: Handler Determines the Method

Based on the HTTP method (POST) and path (/todos), the handler knows to call Create:
// Inside the REST handler:
// POST /todos -> Create method
// GET /todos -> List method
// GET /todos/{id} -> Get method
// DELETE /todos/{id} -> Delete method

methodName := "Create"  // Derived from HTTP method + path
method := svc.Resolve(methodName)  // Find the Method struct

Step 4: Invoker Calls Your Method

The invoker unmarshals the JSON input and calls your method:
// What the invoker does (simplified):
input := &CreateInput{}
json.Unmarshal(requestBody, input)  // {"title": "Buy milk"} -> CreateInput

result, err := yourService.Create(ctx, input)  // Actually calls your code!

Step 5: Handler Sends Response

The handler marshals your response back to JSON:
// Your method returned: &Todo{ID: "todo_1", Title: "Buy milk"}
// Handler converts it to JSON and sends:

HTTP/1.1 200 OK
Content-Type: application/json

{"id":"todo_1","title":"Buy milk","completed":false}

The Complete Request Flow

Here’s the detailed flow for any transport:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        HTTP Request                              β”‚
β”‚  POST /todos                                                     β”‚
β”‚  Body: {"title": "Buy milk"}                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Transport Handler                             β”‚
β”‚                                                                  β”‚
β”‚  1. Parse protocol-specific request                              β”‚
β”‚     REST: Parse HTTP method + path + body                        β”‚
β”‚     JSON-RPC: Parse JSON-RPC envelope                            β”‚
β”‚     MCP: Parse tool call                                         β”‚
β”‚                                                                  β”‚
β”‚  2. Resolve method name                                          β”‚
β”‚     resolver.Resolve("Create") β†’ *Method                         β”‚
β”‚                                                                  β”‚
β”‚  3. Get invoker and call method                                  β”‚
β”‚     invoker.Invoke(ctx, method, inputBytes)                      β”‚
β”‚       β”‚                                                          β”‚
β”‚       β”œβ”€ json.Unmarshal(inputBytes, &CreateInput{})              β”‚
β”‚       β”œβ”€ method.Invoker.Call(ctx, input)                         β”‚
β”‚       └─ return result or error                                  β”‚
β”‚                                                                  β”‚
β”‚  4. Format protocol-specific response                            β”‚
β”‚     REST: JSON body with HTTP status                             β”‚
β”‚     JSON-RPC: JSON-RPC envelope with result/error                β”‚
β”‚     MCP: Content block with isError flag                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        HTTP Response                             β”‚
β”‚  200 OK                                                          β”‚
β”‚  {"id":"todo_1","title":"Buy milk","completed":false}            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Components Explained

Service

The Service struct is the central data structure that holds everything about your API:
type Service struct {
    Name        string      // "todo"
    Description string      // Optional description
    Version     string      // API version
    Methods     []*Method   // All discovered methods
    Types       *TypeRegistry  // All types and their schemas
}

Method

Each method on your service becomes a Method struct:
type Method struct {
    Name        string        // "Create"
    Description string        // From doc comments (if any)
    InputType   reflect.Type  // *CreateInput
    OutputType  reflect.Type  // *Todo
    Invoker     MethodInvoker // Fast caller
}
The Invoker is the key to performance - it’s created once at registration and used for every request.

TypeRegistry

The TypeRegistry holds all your types and their JSON schemas:
// Your Go type:
type CreateInput struct {
    Title string `json:"title"`
}

// Becomes this JSON Schema:
{
    "type": "object",
    "properties": {
        "title": {"type": "string"}
    },
    "required": ["title"]
}
Schemas are used for:
  • OpenAPI documentation
  • MCP tool definitions
  • Input validation (future)
  • Client code generation

Invoker

An invoker is a pre-compiled way to call a method. Without Contract, calling a method via reflection on every request would be slow:
// Slow: reflection on every call
method := reflect.ValueOf(service).MethodByName("Create")
result := method.Call([]reflect.Value{ctxValue, inputValue})
With Contract, the reflection happens once at registration, and subsequent calls are fast:
// Fast: compiled invoker
result, err := method.Invoker.Call(ctx, input)

Package Organization

The contract v2 package is organized into:
contract/v2/
β”œβ”€β”€ contract.go          # Core: Register, RegisteredService
β”œβ”€β”€ types.go             # Type discovery, JSON schema generation
β”œβ”€β”€ errors.go            # Error types and codes
β”‚
└── transport/           # Transport implementations
    β”œβ”€β”€ rest/            # REST transport
    β”‚   └── rest.go      # Mount, NewHandler
    β”‚
    β”œβ”€β”€ jsonrpc/         # JSON-RPC 2.0
    β”‚   └── jsonrpc.go   # Mount, NewHandler, Request/Response types
    β”‚
    β”œβ”€β”€ mcp/             # Model Context Protocol
    β”‚   └── mcp.go       # Mount, NewHandler
    β”‚
    └── openapi/         # OpenAPI spec generation
        └── openapi.go   # Mount, Generate

Design Principles

1. Reflection Only at Startup

Contract uses reflection (Go’s way of inspecting types at runtime) only once when you call Register(). After that, all method calls use pre-compiled invokers. This means:
  • Startup: Slightly slower (milliseconds) due to reflection
  • Runtime: Fast method calls with no reflection overhead

2. Protocol Agnostic Errors

Errors use codes (like NOT_FOUND, INVALID_ARGUMENT) that map to appropriate representations in each protocol:
return nil, contract.ErrNotFound("user not found")
ProtocolRepresentation
RESTHTTP 404 + message body
JSON-RPCError code -32601
MCP{"isError": true, "content": [...]}

3. No Magic, Just Functions

Contract doesn’t use:
  • Code generation
  • Build-time processing
  • Special comments
  • Interface implementations
Everything is standard Go: structs, methods, and function calls. If your code compiles, it works with Contract.

Common Questions

Why not use code generation instead of reflection?

Code generation (like protobuf) requires extra build steps and generated files to maintain. Contract’s reflection-based approach:
  • Works with any Go struct immediately
  • No extra build steps
  • No generated files to keep in sync
  • Faster iteration during development

How does Contract know which HTTP verb to use?

For REST, Contract uses naming conventions:
  • Create* β†’ POST
  • Get* β†’ GET
  • List* β†’ GET
  • Update* β†’ PUT
  • Delete* β†’ DELETE
  • Other names β†’ POST

Can I use Contract with gRPC?

Not directly yet, but the error codes are aligned with gRPC status codes for future compatibility. JSON-RPC provides similar RPC semantics over HTTP.

What happens if I change my method signature?

Re-register your service (which happens automatically on server restart). Contract will discover the new signature and update the schemas.

See Also