Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/platforma-dev/platforma/llms.txt

Use this file to discover all available pages before exploring further.

Domains are the core organizational unit in Platforma. Each domain represents a bounded context - a distinct area of your business logic with its own data models, business rules, and APIs.

Domain Interface

All domains must implement the simple interface from application/domain.go:4:
type Domain interface {
    GetRepository() any
}
This allows the application to access the domain’s repository for database migration registration.
The GetRepository() method returns any because different domains have different repository types. The application only needs the repository reference to call its Migrations() method.

Domain Structure

A typical domain aggregates four components:
type Domain struct {
    Repository  *Repository               // Database access
    Service     *Service                  // Business logic
    HandleGroup *httpserver.HandlerGroup  // HTTP API (optional)
    Middleware  httpserver.Middleware     // Request interceptors (optional)
}

Component Breakdown

Handles all database operations for the domain:
  • CRUD operations
  • Complex queries
  • Database migrations
  • Transaction management
Dependencies: Only the database connection
Implements domain business rules:
  • Validation logic
  • Orchestrates repository calls
  • Coordinates with other domains
  • Contains no HTTP or database code
Dependencies: Repository and other domain services
Exposes HTTP endpoints for the domain:
  • Route definitions
  • Request/response handling
  • Input validation
  • Error mapping
Dependencies: Service layer
Intercepts HTTP requests:
  • Authentication/authorization
  • Request validation
  • Context enrichment
  • Response modification
Dependencies: Service layer (often)

Real Example: Auth Domain

The auth package provides a complete reference implementation. From auth/domain.go:7:
type Domain struct {
    Repository  *Repository
    Service     *Service
    HandleGroup *httpserver.HandlerGroup
    Middleware  httpserver.Middleware
}

// Implement Domain interface
func (d *Domain) GetRepository() any {
    return d.Repository
}

Auth Domain Constructor

From auth/domain.go:18, showing full domain initialization:
func New(
    db db,
    authStorage authStorage,
    sessionCookieName string,
    usernameValidator, passwordValidator func(string) error,
    cleanupEnqueuer cleanupEnqueuer,
) *Domain {
    // Initialize repository
    repository := NewRepository(db)
    
    // Initialize service with dependencies
    service := NewService(
        repository,
        authStorage,
        sessionCookieName,
        usernameValidator,
        passwordValidator,
        cleanupEnqueuer,
    )

    // Create middleware
    authMiddleware := NewAuthenticationMiddleware(service)
    
    // Create HTTP handlers
    registerHandler := NewRegisterHandler(service)
    loginHandler := NewLoginHandler(service)
    logoutHandler := NewLogoutHandler(service)
    getUserHandler := NewGetHandler(service)
    changePasswordHandler := authMiddleware.Wrap(NewChangePasswordHandler(service))
    deleteHandler := authMiddleware.Wrap(NewDeleteHandler(service))

    // Build API handler group
    authAPI := httpserver.NewHandlerGroup()
    authAPI.Handle("POST /register", registerHandler)
    authAPI.Handle("POST /login", loginHandler)
    authAPI.Handle("POST /logout", logoutHandler)
    authAPI.Handle("GET /me", getUserHandler)
    authAPI.Handle("POST /change-password", changePasswordHandler)
    authAPI.Handle("DELETE /me", deleteHandler)

    return &Domain{
        Repository:  repository,
        Service:     service,
        HandleGroup: authAPI,
        Middleware:  authMiddleware,
    }
}
Notice how the constructor uses dependency injection - all external dependencies are passed as parameters, making the domain testable and flexible.

Creating a Domain

Follow these steps to create a new domain:
1

Define your domain struct

Create a domain.go file:
package myfeature

import "github.com/platforma-dev/platforma/httpserver"

type Domain struct {
    Repository  *Repository
    Service     *Service
    HandleGroup *httpserver.HandlerGroup
}
2

Implement the Domain interface

Add the required method:
func (d *Domain) GetRepository() any {
    return d.Repository
}
3

Create a constructor

Initialize all components:
func New(db *database.Database) *Domain {
    repo := NewRepository(db)
    svc := NewService(repo)
    
    api := httpserver.NewHandlerGroup()
    api.Handle("GET /items", NewGetItemsHandler(svc))
    api.Handle("POST /items", NewCreateItemHandler(svc))
    
    return &Domain{
        Repository:  repo,
        Service:     svc,
        HandleGroup: api,
    }
}
4

Register with application

In your main.go:
domain := myfeature.New(db)
app.RegisterDomain("myfeature", "main", domain)

Repository Pattern

Repositories handle all database operations:
type Repository struct {
    db *database.Database
}

func NewRepository(db *database.Database) *Repository {
    return &Repository{db: db}
}

// Database operations
func (r *Repository) GetByID(ctx context.Context, id string) (*Item, error) {
    query := `SELECT id, name, created_at FROM items WHERE id = $1`
    
    var item Item
    err := r.db.GetContext(ctx, &item, query, id)
    if err != nil {
        return nil, err
    }
    
    return &item, nil
}

// Expose migrations
func (r *Repository) Migrations() []database.Migration {
    return migrations
}
Repositories should only interact with the database. No business logic, HTTP handling, or external API calls.

Service Pattern

Services implement business logic and orchestrate repository calls:
type Service struct {
    repo *Repository
}

func NewService(repo *Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) CreateItem(ctx context.Context, name string) (*Item, error) {
    // Validation (business logic)
    if len(name) < 3 {
        return nil, errors.New("name too short")
    }
    
    // Use repository for data access
    item := &Item{
        ID:        uuid.New(),
        Name:      name,
        CreatedAt: time.Now(),
    }
    
    if err := s.repo.Create(ctx, item); err != nil {
        return nil, fmt.Errorf("failed to create item: %w", err)
    }
    
    return item, nil
}

Mounting Domain APIs

Mount your domain’s HTTP API to a server:
// Create domain
authDomain := auth.New(db, sessionStore, "session", 
    validateUsername, validatePassword, enqueuer)

// Mount to HTTP server
api := httpserver.New("8080", 5*time.Second)
api.HandleGroup("/auth", authDomain.HandleGroup)

// Register server
app.RegisterService("api", api)
Now all auth routes are available under /auth:
  • POST /auth/register
  • POST /auth/login
  • GET /auth/me
  • etc.
Domains are self-contained. You can mount the same domain to multiple servers or use it in different applications.

Using Domain Middleware

Apply domain middleware to protect routes:
// Protect entire handler group
protectedAPI := httpserver.NewHandlerGroup()
protectedAPI.Use(authDomain.Middleware)
protectedAPI.Handle("GET /profile", getProfileHandler)
protectedAPI.Handle("POST /settings", updateSettingsHandler)

api.HandleGroup("/user", protectedAPI)
Or wrap individual handlers from auth/domain.go:27:
changePasswordHandler := authMiddleware.Wrap(NewChangePasswordHandler(service))

Domain Dependencies

Domains can depend on other domains via service interfaces:
// Define interface for what you need
type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

// Accept interface in constructor
func NewOrderService(repo *Repository, users UserService) *Service {
    return &Service{
        repo:  repo,
        users: users,
    }
}

// Wire in main.go
userDomain := user.New(db)
orderDomain := order.New(db, userDomain.Service)
Depend on interfaces, not concrete types. This keeps domains loosely coupled and makes testing easier.

Testing Domains

Test each layer independently:
// Test repository with real database
func TestRepository_GetByID(t *testing.T) {
    db := setupTestDB(t)
    repo := NewRepository(db)
    
    item, err := repo.GetByID(context.Background(), "test-id")
    // assertions...
}

// Test service with mock repository
type mockRepository struct{}
func (m *mockRepository) GetByID(ctx context.Context, id string) (*Item, error) {
    return &Item{ID: id, Name: "test"}, nil
}

func TestService_CreateItem(t *testing.T) {
    repo := &mockRepository{}
    svc := NewService(repo)
    
    item, err := svc.CreateItem(context.Background(), "test")
    // assertions...
}

Package Organization

Recommended file structure for a domain:
myfeature/
├── domain.go       # Domain struct + constructor + GetRepository()
├── model.go        # Data models and types
├── repository.go   # Database operations
├── service.go      # Business logic
├── handler_*.go    # HTTP handlers (one per endpoint)
├── middleware.go   # HTTP middleware (if needed)
├── context.go      # Context helpers
├── errors.go       # Domain-specific errors
└── migrations/     # SQL migration files
    ├── 001_init.sql
    └── 002_add_column.sql

Next Steps

HTTP Routing

Learn how to define domain APIs with HandlerGroup

Middleware

Create middleware for authentication and validation

Database

Set up repositories and migrations

Architecture

Understand the complete framework architecture

Build docs developers (and LLMs) love