Skip to main content

The Interface-First Approach

In Contract, a service is defined by two parts:
  1. An interface - Declares what operations your API supports (the “what”)
  2. An implementation - Contains the actual business logic (the “how”)
This separation is intentional and powerful. The interface is your API contract - it’s the source of truth for what your service can do. The implementation is a detail that clients don’t need to know about. We recommend organizing each service in its own Go package. This lets you use clean, simple names like API and Service - the package name provides the context:
package todo

import "context"

// API defines the contract for todo operations.
// When imported, this becomes "todo.API" which reads naturally.
type API interface {
    Create(ctx context.Context, in *CreateInput) (*Todo, error)
    List(ctx context.Context) (*ListOutput, error)
}

// Service implements todo.API with actual business logic.
// When imported, this becomes "todo.Service".
type Service struct {
    db *sql.DB  // Dependencies go here
}

func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    // Your business logic here
}

Why Interface-First?

Before diving into the details, let’s understand why this approach is valuable.

Compile-Time Safety

If your implementation doesn’t match the interface, Go’s compiler tells you immediately - before your code even runs:
package todo

type API interface {
    Create(ctx context.Context, in *CreateInput) (*Todo, error)
}

type Service struct{}

// This won't compile!
// Error: cannot use &Service{} (type *Service) as type API
// missing method Create

// You can add a compile-time check explicitly:
var _ API = (*Service)(nil)  // Fails if Service doesn't implement API
The compiler catches mismatches before your code even runs. This is a huge advantage over dynamic approaches where you might only discover problems when users hit a bug in production.

Clear Contracts

The interface IS your API documentation. Anyone can read it and understand exactly what your service does, without digging through implementation details:
package user

// API defines all operations for user management.
// Reading this interface tells you everything about what the service can do.
type API interface {
    // Create registers a new user account.
    // Returns the created user with a generated ID.
    Create(ctx context.Context, in *CreateInput) (*User, error)

    // Get retrieves a user by their unique ID.
    // Returns NOT_FOUND error if the user doesn't exist.
    Get(ctx context.Context, in *GetInput) (*User, error)

    // Update modifies an existing user's profile.
    // Only fields provided in the input are changed.
    Update(ctx context.Context, in *UpdateInput) (*User, error)

    // Delete removes a user account permanently.
    // This operation cannot be undone.
    Delete(ctx context.Context, in *DeleteInput) error

    // List returns all users with pagination support.
    List(ctx context.Context, in *ListInput) (*ListOutput, error)
}

Easy Testing

Because the interface separates “what” from “how”, you can easily create mock implementations for testing:
package user_test

import "yourapp/user"

// MockService implements user.API for testing.
// It stores users in memory instead of a real database.
type MockService struct {
    users map[string]*user.User
}

func (m *MockService) Get(ctx context.Context, in *user.GetInput) (*user.User, error) {
    if u, ok := m.users[in.ID]; ok {
        return u, nil
    }
    return nil, errors.New("not found")
}

func (m *MockService) Create(ctx context.Context, in *user.CreateInput) (*user.User, error) {
    u := &user.User{ID: "test-123", Email: in.Email}
    m.users[u.ID] = u
    return u, nil
}

// ... implement other methods

// Now use MockService in your tests!
func TestSomethingThatNeedsUsers(t *testing.T) {
    mock := &MockService{users: make(map[string]*user.User)}
    // Use mock instead of real service
}

Multiple Implementations

You can have different implementations for different situations - all sharing the same interface:
package storage

// API defines storage operations.
// The same interface can have multiple implementations.
type API interface {
    Save(ctx context.Context, in *SaveInput) error
    Load(ctx context.Context, in *LoadInput) (*Data, error)
}

// FileService stores data on the local filesystem.
// Good for development and single-server deployments.
type FileService struct {
    basePath string
}

// S3Service stores data in Amazon S3.
// Good for production with multiple servers.
type S3Service struct {
    bucket string
    client *s3.Client
}

// MemoryService stores data in memory.
// Perfect for tests - fast and isolated.
type MemoryService struct {
    data map[string][]byte
}

// All three implement storage.API!
// Your application can switch between them based on configuration.

Package Organization

We recommend organizing each service in its own package with a consistent structure:
yourapp/
├── main.go              # Wires everything together
├── todo/
│   ├── api.go           # Interface definition (todo.API)
│   ├── service.go       # Implementation (todo.Service)
│   └── types.go         # Input/output types
├── user/
│   ├── api.go           # Interface definition (user.API)
│   ├── service.go       # Implementation (user.Service)
│   └── types.go         # Input/output types
Why this structure?
  1. Package-based naming: todo.API is clearer than TodoAPI, and user.Service is clearer than userService
  2. Encapsulation: Each package is self-contained with its own types
  3. Discoverability: Finding the todo API? Look in the todo package
  4. Scalability: Adding a new service? Create a new package

Defining Your Interface

Basic Structure

Every interface method must follow specific patterns. The rules are straightforward:
package myservice

type API interface {
    // Every method MUST have ctx as the first parameter
    // This is required for timeouts, cancellation, and request-scoped values
    MethodName(ctx context.Context, ...) ...
}

Method Signature Patterns

Contract supports four method patterns. Let’s explore each one with detailed explanations.

Pattern 1: Input and Output

The most common pattern - receive data, return data:
func MethodName(ctx context.Context, in *InputType) (*OutputType, error)
Breakdown:
  • ctx context.Context - Required first parameter for request context
  • in *InputType - Pointer to a struct containing the input data
  • *OutputType - Pointer to a struct containing the result
  • error - Always the last return value; nil means success
Example:
package order

type API interface {
    // Create places a new order.
    // Input: order details (items, customer, shipping address)
    // Output: the created order with generated ID and totals
    Create(ctx context.Context, in *CreateInput) (*Order, error)

    // Get retrieves order details by ID.
    // Input: the order ID to look up
    // Output: full order information
    Get(ctx context.Context, in *GetInput) (*Order, error)

    // CalculateShipping determines shipping cost.
    // Input: destination and items
    // Output: available shipping options with prices
    CalculateShipping(ctx context.Context, in *ShippingInput) (*ShippingOutput, error)
}
When to use: Most CRUD operations, calculations, queries - any method that receives data and returns data.

Pattern 2: Output Only (No Input)

Return data without receiving any input parameters:
func MethodName(ctx context.Context) (*OutputType, error)
Breakdown:
  • No input parameter beyond context
  • Still returns output and error
Example:
package dashboard

type API interface {
    // GetStats returns current system statistics.
    // No input needed - just returns current stats.
    GetStats(ctx context.Context) (*StatsOutput, error)

    // ListAll returns all items without any filters.
    // Useful for small datasets or admin interfaces.
    ListAll(ctx context.Context) (*ListOutput, error)

    // Health returns detailed health information.
    // More detailed than a simple ping.
    Health(ctx context.Context) (*HealthOutput, error)
}
When to use: Listing all items, getting current status, dashboard data, health checks that return information.

Pattern 3: Input Only (No Output)

Accept input but return only success/failure:
func MethodName(ctx context.Context, in *InputType) error
Breakdown:
  • Accepts input data
  • Only returns error (nil means success)
  • No meaningful data to return
Example:
package notification

type API interface {
    // Send delivers a notification to a user.
    // We don't need to return anything - just success/failure.
    Send(ctx context.Context, in *SendInput) error

    // Delete removes a notification by ID.
    // Nothing to return after deletion.
    Delete(ctx context.Context, in *DeleteInput) error

    // MarkAllRead marks all notifications as read for a user.
    // Fire-and-forget operation.
    MarkAllRead(ctx context.Context, in *MarkReadInput) error
}
When to use: Delete operations, fire-and-forget updates, operations where you only care about success/failure.

Pattern 4: No Input or Output

Just do something and report success/failure:
func MethodName(ctx context.Context) error
Breakdown:
  • Only context as input
  • Only error as output
  • Simplest possible signature
Example:
package maintenance

type API interface {
    // Ping checks if the service is responsive.
    // Returns nil if healthy, error if not.
    Ping(ctx context.Context) error

    // Cleanup runs maintenance tasks.
    // No parameters needed, no results to return.
    Cleanup(ctx context.Context) error

    // CheckDB verifies database connectivity.
    // Simple health check that returns error if DB is down.
    CheckDB(ctx context.Context) error
}
When to use: Health checks, ping endpoints, administrative operations with no configuration.

Defining Input and Output Types

Types define the shape of data that flows through your API. Good type design makes your API clear and easy to use.

Input Types

Input types describe what clients send to your methods. They should only contain fields that clients actually need to provide:
package user

// CreateInput contains everything needed to create a new user.
// Notice we don't include ID - that's generated by the server.
type CreateInput struct {
    // Required fields - must be provided by the client
    Email    string `json:"email"`    // User's email address
    Password string `json:"password"` // Plain text (we'll hash it)
    Name     string `json:"name"`     // Display name

    // Optional fields - zero value if not provided
    Phone    string `json:"phone,omitempty"`  // "" if not sent
    Avatar   string `json:"avatar,omitempty"` // "" if not sent
}
Key points:
  • Use pointer types in method signatures: *CreateInput
  • Use json tags to control field names in JSON
  • Add omitempty for optional fields (tells JSON encoder to skip empty values)
  • Don’t include server-generated fields (like ID, CreatedAt)

Output Types

Output types describe what you return to clients. They can include computed and server-generated fields:
package user

// User represents a user in the system.
// This is what clients receive back from the API.
type User struct {
    ID        string    `json:"id"`        // Server-generated unique identifier
    Email     string    `json:"email"`     // From input
    Name      string    `json:"name"`      // From input
    CreatedAt time.Time `json:"createdAt"` // Server-generated timestamp
    UpdatedAt time.Time `json:"updatedAt"` // Server-managed timestamp
}

// ListOutput wraps a list of users with metadata.
// Wrapping in a struct lets us add pagination info.
type ListOutput struct {
    Users []*User `json:"users"` // The actual user data
    Total int     `json:"total"` // Total count for pagination
    Page  int     `json:"page"`  // Current page number
}

Why Separate Input and Output Types?

You might wonder why we don’t just use one User type everywhere. Here’s why separation is valuable:
package todo

// BAD: Using same type for input and output
// Problems:
// - Clients might try to set ID (server-generated)
// - Clients might try to set CreatedAt (server-managed)
// - No way to distinguish required vs optional fields
type Todo struct {
    ID        string    `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"createdAt"`
}

// GOOD: Separate types for different purposes
// CreateInput - only what clients provide
type CreateInput struct {
    Title string `json:"title"` // Only field needed to create
}

// UpdateInput - what can be updated
type UpdateInput struct {
    ID        string `json:"id"`        // Which todo to update
    Title     string `json:"title"`     // Can change title
    Completed bool   `json:"completed"` // Can change status
}

// Todo - full representation with server data
type Todo struct {
    ID        string    `json:"id"`        // Server-generated
    Title     string    `json:"title"`     // From input
    Completed bool      `json:"completed"` // From input or default
    CreatedAt time.Time `json:"createdAt"` // Server-generated
    UpdatedAt time.Time `json:"updatedAt"` // Server-managed
}

Nested Types

For complex data, nest types within each other. This creates clear, reusable structures:
package order

// Order represents a complete order with all its details.
type Order struct {
    ID       string     `json:"id"`
    Customer *Customer  `json:"customer"`  // Nested customer info
    Items    []*Item    `json:"items"`     // List of ordered items
    Shipping *Address   `json:"shipping"`  // Where to ship
    Billing  *Address   `json:"billing"`   // Billing address (can reuse Address)
    Total    Money      `json:"total"`     // Order total
}

// Customer contains customer information.
type Customer struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Item represents one line item in an order.
type Item struct {
    ProductID string `json:"productId"`
    Name      string `json:"name"`
    Quantity  int    `json:"quantity"`
    Price     Money  `json:"price"`
}

// Address is a physical address.
// Reused for both shipping and billing.
type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    State   string `json:"state"`
    Country string `json:"country"`
    Zip     string `json:"zip"`
}

// Money represents a monetary amount.
type Money struct {
    Amount   int64  `json:"amount"`   // Amount in cents
    Currency string `json:"currency"` // ISO currency code
}

Implementing Your Interface

Now let’s implement the interface with actual business logic.

Basic Implementation

Create a struct that implements all interface methods:
package todo

import (
    "context"
    "database/sql"
    "time"
)

// Service implements todo.API.
// It contains dependencies needed to perform operations.
type Service struct {
    db *sql.DB  // Database connection
}

// Compile-time check that Service implements API.
// If Service is missing methods, this line causes a compile error.
var _ API = (*Service)(nil)

// NewService creates a new todo Service with the given dependencies.
// This is the "constructor" - the standard Go pattern for creating instances.
func NewService(db *sql.DB) *Service {
    return &Service{db: db}
}

// Create implements todo.API.Create
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    // Validate input
    if in.Title == "" {
        return nil, errors.New("title is required")
    }

    // Create the todo
    now := time.Now()
    todo := &Todo{
        ID:        generateID(),
        Title:     in.Title,
        Completed: false,
        CreatedAt: now,
        UpdatedAt: now,
    }

    // Save to database
    _, err := s.db.ExecContext(ctx,
        "INSERT INTO todos (id, title, completed, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
        todo.ID, todo.Title, todo.Completed, todo.CreatedAt, todo.UpdatedAt,
    )
    if err != nil {
        return nil, err
    }

    return todo, nil
}

// List implements todo.API.List
func (s *Service) List(ctx context.Context) (*ListOutput, error) {
    rows, err := s.db.QueryContext(ctx, "SELECT id, title, completed, created_at, updated_at FROM todos")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var todos []*Todo
    for rows.Next() {
        t := &Todo{}
        if err := rows.Scan(&t.ID, &t.Title, &t.Completed, &t.CreatedAt, &t.UpdatedAt); err != nil {
            return nil, err
        }
        todos = append(todos, t)
    }

    return &ListOutput{
        Items: todos,
        Count: len(todos),
    }, nil
}

// ... implement other methods

With Multiple Dependencies

Real services often need multiple dependencies. Inject them all through the constructor:
package user

import (
    "context"
    "database/sql"
    "log/slog"

    "github.com/redis/go-redis/v9"
)

// Service implements user.API with all required dependencies.
type Service struct {
    db     *sql.DB         // Primary database
    cache  *redis.Client   // Cache for frequent lookups
    mailer EmailSender     // For sending emails (interface for testability)
    logger *slog.Logger    // Structured logging
}

// EmailSender is an interface so we can mock it in tests.
type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

// NewService creates a new user Service with all dependencies.
func NewService(
    db *sql.DB,
    cache *redis.Client,
    mailer EmailSender,
    logger *slog.Logger,
) *Service {
    return &Service{
        db:     db,
        cache:  cache,
        mailer: mailer,
        logger: logger,
    }
}

func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) {
    s.logger.Info("creating user", "email", in.Email)

    // Create user in database
    user := &User{ID: generateID(), Email: in.Email, Name: in.Name}
    if err := s.saveToDatabase(ctx, user); err != nil {
        s.logger.Error("failed to save user", "error", err)
        return nil, err
    }

    // Cache the user for fast lookups
    if err := s.cacheUser(ctx, user); err != nil {
        s.logger.Warn("failed to cache user", "error", err)
        // Don't fail the request - caching is optional
    }

    // Send welcome email
    if err := s.mailer.Send(ctx, user.Email, "Welcome!", "Thanks for signing up!"); err != nil {
        s.logger.Warn("failed to send welcome email", "error", err)
        // Don't fail the request - email is best-effort
    }

    return user, nil
}

Compile-Time Interface Check

Always add a compile-time check to ensure your implementation is complete. This catches missing methods immediately:
package todo

type API interface {
    Create(ctx context.Context, in *CreateInput) (*Todo, error)
    List(ctx context.Context) (*ListOutput, error)
    Get(ctx context.Context, in *GetInput) (*Todo, error)
    Delete(ctx context.Context, in *DeleteInput) error
}

type Service struct {
    db *sql.DB
}

// This line is a compile-time assertion.
// If Service doesn't implement all methods of API, compilation fails.
// The syntax works because:
// - (*Service)(nil) creates a nil pointer of type *Service
// - var _ API = ... checks if that pointer satisfies the API interface
var _ API = (*Service)(nil)

// Now implement the methods...
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    // ...
}
If you forget to implement a method, you’ll see a clear error:
cannot use (*Service)(nil) (type *Service) as type API in assignment:
    *Service does not implement API (missing Delete method)

Method Naming Conventions

Method names affect how they’re exposed via REST. Contract uses naming conventions to determine HTTP methods:
Method Name PatternREST EndpointHTTP Method
Create, Add, New/{resource}POST
List, All, Search/{resource}GET
Get, Find, Fetch/{resource}/{id}GET
Update, Edit, Set/{resource}/{id}PUT
Delete, Remove/{resource}/{id}DELETE
Patch/{resource}/{id}PATCH
Other names/{resource}/{action}POST
Examples:
package product

type API interface {
    Create(...)     // POST   /products         - Create new product
    List(...)       // GET    /products         - List all products
    Get(...)        // GET    /products/{id}    - Get one product
    Update(...)     // PUT    /products/{id}    - Update product
    Delete(...)     // DELETE /products/{id}    - Delete product
    Search(...)     // GET    /products         - Search (with query params)
    Archive(...)    // POST   /products/archive - Custom action
}

Using Context

Every method receives context.Context as its first parameter. Context is Go’s standard way to pass request-scoped data and control request lifecycle.

Cancellation

Check if the client cancelled the request. This is important for long-running operations:
func (s *Service) ProcessLargeDataset(ctx context.Context, in *ProcessInput) (*Output, error) {
    for i, item := range in.Items {
        // Check if context was cancelled before each item
        if err := ctx.Err(); err != nil {
            // Client disconnected or timeout occurred
            return nil, err
        }

        // Process the item
        if err := s.processItem(ctx, item); err != nil {
            return nil, err
        }
    }
    return &Output{ProcessedCount: len(in.Items)}, nil
}

Timeouts

Pass context to database and external calls. The context carries timeout information:
func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) {
    // The context timeout is automatically respected by the database driver
    row := s.db.QueryRowContext(ctx, "SELECT * FROM todos WHERE id = $1", in.ID)

    todo := &Todo{}
    err := row.Scan(&todo.ID, &todo.Title, &todo.Completed)
    if err == sql.ErrNoRows {
        return nil, contract.ErrNotFound("todo not found")
    }
    return todo, err
}

Request-Scoped Values

Access values set by middleware (like authenticated user):
// Key type for context values (prevents collisions)
type contextKey string

const userIDKey contextKey = "userID"

func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    // Get authenticated user ID from context (set by auth middleware)
    userID, ok := ctx.Value(userIDKey).(string)
    if !ok {
        return nil, contract.ErrUnauthenticated("not logged in")
    }

    // Use userID in your logic
    todo := &Todo{
        ID:      generateID(),
        Title:   in.Title,
        OwnerID: userID,  // Associate todo with the user
    }

    // Save and return
    return s.saveTodo(ctx, todo)
}

Complete Example

Here’s a complete, production-ready service demonstrating all the concepts:
// user/types.go
package user

import "time"

// User represents a user in the system.
type User struct {
    ID        string    `json:"id"`
    Email     string    `json:"email"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"createdAt"`
    UpdatedAt time.Time `json:"updatedAt"`
}

// CreateInput contains data needed to create a user.
type CreateInput struct {
    Email string `json:"email"`
    Name  string `json:"name"`
}

// GetInput identifies which user to retrieve.
type GetInput struct {
    ID string `json:"id"`
}

// UpdateInput contains data for updating a user.
type UpdateInput struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// DeleteInput identifies which user to delete.
type DeleteInput struct {
    ID string `json:"id"`
}

// ListInput contains pagination parameters.
type ListInput struct {
    Limit  int `json:"limit"`
    Offset int `json:"offset"`
}

// ListOutput contains paginated user results.
type ListOutput struct {
    Users []*User `json:"users"`
    Total int     `json:"total"`
}
// user/api.go
package user

import "context"

// API defines all user management operations.
type API interface {
    // Create registers a new user account.
    Create(ctx context.Context, in *CreateInput) (*User, error)

    // Get retrieves a user by ID.
    Get(ctx context.Context, in *GetInput) (*User, error)

    // Update modifies a user's profile.
    Update(ctx context.Context, in *UpdateInput) (*User, error)

    // Delete removes a user account.
    Delete(ctx context.Context, in *DeleteInput) error

    // List returns users with pagination.
    List(ctx context.Context, in *ListInput) (*ListOutput, error)
}
// user/service.go
package user

import (
    "context"
    "database/sql"
    "time"

    "github.com/google/uuid"
    contract "github.com/go-mizu/mizu/contract/v2"
)

// Service implements user.API.
type Service struct {
    db *sql.DB
}

// Compile-time interface check.
var _ API = (*Service)(nil)

// NewService creates a new user Service.
func NewService(db *sql.DB) *Service {
    return &Service{db: db}
}

func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) {
    // Validate input
    if in.Email == "" {
        return nil, contract.ErrInvalidArgument("email is required")
    }
    if in.Name == "" {
        return nil, contract.ErrInvalidArgument("name is required")
    }

    // Create user
    now := time.Now()
    user := &User{
        ID:        uuid.New().String(),
        Email:     in.Email,
        Name:      in.Name,
        CreatedAt: now,
        UpdatedAt: now,
    }

    // Save to database
    _, err := s.db.ExecContext(ctx,
        "INSERT INTO users (id, email, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)",
        user.ID, user.Email, user.Name, user.CreatedAt, user.UpdatedAt,
    )
    if err != nil {
        return nil, err
    }

    return user, nil
}

func (s *Service) Get(ctx context.Context, in *GetInput) (*User, error) {
    user := &User{}
    err := s.db.QueryRowContext(ctx,
        "SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1",
        in.ID,
    ).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt)

    if err == sql.ErrNoRows {
        return nil, contract.ErrNotFound("user not found")
    }
    if err != nil {
        return nil, err
    }

    return user, nil
}

func (s *Service) Update(ctx context.Context, in *UpdateInput) (*User, error) {
    now := time.Now()

    result, err := s.db.ExecContext(ctx,
        "UPDATE users SET name = $1, updated_at = $2 WHERE id = $3",
        in.Name, now, in.ID,
    )
    if err != nil {
        return nil, err
    }

    rows, _ := result.RowsAffected()
    if rows == 0 {
        return nil, contract.ErrNotFound("user not found")
    }

    return s.Get(ctx, &GetInput{ID: in.ID})
}

func (s *Service) Delete(ctx context.Context, in *DeleteInput) error {
    result, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id = $1", in.ID)
    if err != nil {
        return err
    }

    rows, _ := result.RowsAffected()
    if rows == 0 {
        return contract.ErrNotFound("user not found")
    }

    return nil
}

func (s *Service) List(ctx context.Context, in *ListInput) (*ListOutput, error) {
    // Default limit
    limit := in.Limit
    if limit <= 0 {
        limit = 10
    }

    // Query users
    rows, err := s.db.QueryContext(ctx,
        "SELECT id, email, name, created_at, updated_at FROM users LIMIT $1 OFFSET $2",
        limit, in.Offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []*User
    for rows.Next() {
        user := &User{}
        if err := rows.Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt); err != nil {
            return nil, err
        }
        users = append(users, user)
    }

    // Get total count
    var total int
    s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&total)

    return &ListOutput{
        Users: users,
        Total: total,
    }, nil
}

Best Practices

Keep Services Focused

Each service should handle one domain. Don’t create “god services” that do everything:
// GOOD: Focused services, one domain each
package todo    // type API interface { Create, List, Get, Delete }
package user    // type API interface { Create, Get, Update, Delete }
package order   // type API interface { Create, Get, List, Cancel }

// BAD: One giant service doing everything
package api
type EverythingAPI interface {
    CreateTodo(...)
    CreateUser(...)
    CreateOrder(...)
    // 50 more methods from different domains...
}

Use Clear, Consistent Naming

Names should be descriptive and follow a consistent pattern:
// GOOD: Clear and consistent naming
package subscription

type API interface {
    Create(ctx context.Context, in *CreateInput) (*Subscription, error)
    Get(ctx context.Context, in *GetInput) (*Subscription, error)
    Cancel(ctx context.Context, in *CancelInput) error
    Renew(ctx context.Context, in *RenewInput) (*Subscription, error)
}

// BAD: Vague and inconsistent naming
package sub

type SubAPI interface {
    MakeSub(ctx context.Context, in *Input) (*Output, error)
    FetchSubscription(ctx context.Context, in *GetInput) (*Sub, error)
    DoCancel(ctx context.Context, in *CancelInput) error
}

Inject Dependencies

Pass dependencies through the constructor, never use globals:
// GOOD: Dependency injection through constructor
type Service struct {
    db    Database
    cache Cache
}

func NewService(db Database, cache Cache) *Service {
    return &Service{db: db, cache: cache}
}

// BAD: Global dependencies
var globalDB *sql.DB  // DON'T DO THIS

func (s *Service) Create(...) {
    globalDB.Query(...)  // Untestable, hidden dependency
}

Keep Business Logic in the Implementation

The interface should only declare operations. All logic goes in the implementation:
// Interface: Pure contract, no logic
type API interface {
    ProcessPayment(ctx context.Context, in *PaymentInput) (*Payment, error)
}

// Implementation: All the business logic
func (s *Service) ProcessPayment(ctx context.Context, in *PaymentInput) (*Payment, error) {
    // 1. Validation
    if in.Amount <= 0 {
        return nil, contract.ErrInvalidArgument("amount must be positive")
    }

    // 2. Business rules
    fee := s.calculateFee(in.Amount)
    total := in.Amount + fee

    // 3. External integrations
    chargeResult, err := s.stripe.Charge(ctx, in.CardToken, total)
    if err != nil {
        return nil, fmt.Errorf("payment failed: %w", err)
    }

    // 4. Persistence
    payment := &Payment{
        ID:     chargeResult.ID,
        Amount: in.Amount,
        Fee:    fee,
        Status: "completed",
    }
    if err := s.db.SavePayment(ctx, payment); err != nil {
        return nil, err
    }

    return payment, nil
}

Common Questions

Can I have multiple return values?

No, methods can only return (*Output, error) or error. If you need to return multiple values, wrap them in a struct:
// Instead of: func Get() (*User, *Profile, error)  // NOT SUPPORTED
// Use a wrapper struct:

type GetUserResult struct {
    User    *User    `json:"user"`
    Profile *Profile `json:"profile"`
}

func (s *Service) GetUser(ctx context.Context, in *GetInput) (*GetUserResult, error) {
    user, err := s.getUser(ctx, in.ID)
    if err != nil {
        return nil, err
    }
    profile, err := s.getProfile(ctx, in.ID)
    if err != nil {
        return nil, err
    }
    return &GetUserResult{User: user, Profile: profile}, nil
}

Should input types be pointers?

Yes, always use pointers in the method signature:
// GOOD - use pointers
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Output, error)

// BAD - don't use value types
func (s *Service) Create(ctx context.Context, in CreateInput) (Output, error)
Contract requires pointers for proper JSON unmarshaling and to support nil inputs.

How do I handle optional fields?

Fields not provided by clients will have their zero value. For most cases, this works fine:
type UpdateInput struct {
    ID    string `json:"id"`    // Required - must be provided
    Title string `json:"title"` // Optional - "" if not sent
    Count int    `json:"count"` // Optional - 0 if not sent
}
For fields where you need to distinguish “not sent” from “sent as empty/zero”, use pointers:
type UpdateInput struct {
    ID    string  `json:"id"`
    Title *string `json:"title"` // nil = not sent, "" = sent as empty
    Count *int    `json:"count"` // nil = not sent, 0 = sent as zero
}

func (s *Service) Update(ctx context.Context, in *UpdateInput) (*Todo, error) {
    todo, _ := s.Get(ctx, &GetInput{ID: in.ID})

    // Only update if field was explicitly sent
    if in.Title != nil {
        todo.Title = *in.Title
    }
    if in.Count != nil {
        todo.Count = *in.Count
    }

    return s.save(ctx, todo)
}

What’s Next?

Now that you know how to define services: