Skip to main content
This guide covers best practices and conventions for developing Temporal Server.

Code Style

Follow Project Conventions

Rigorously adhere to existing project conventions. Always analyze surrounding code, tests, and configuration before making changes.
Key principles:
  • Mimic the style (formatting, naming), structure, and architectural patterns of existing code
  • Understand local context (imports, functions, interfaces) to ensure changes integrate naturally
  • Never introduce new patterns without discussing with maintainers

Comments

Add comments sparingly. Focus on why something is done, not what is done:
// Good: Explains why
// Use buffered channel to prevent goroutine leak if context is cancelled
ch := make(chan result, 1)

// Bad: Explains what (obvious from code)
// Create a channel for results
ch := make(chan result, 1)
For future design considerations, use:
// CONSIDER(username): We might want to batch these operations for better performance

Formatting

Use the project’s formatting tools:
# Format imports
make fmt-imports

# Run go fix
make fmt-gofix

# Format YAML files
make fmt-yaml

# Format everything
make fmt

Error Handling

Always Handle Errors

Errors MUST be handled, never ignored.
// Good
if err := doSomething(); err != nil {
    return fmt.Errorf("failed to do something: %w", err)
}

// Bad - DO NOT DO THIS
_ = doSomething()
doSomething() // Ignored error

Logging Errors

Use appropriate logging methods based on severity:
// For core invariant violations that should never happen
logger.Fatal("invariant violation", tag.Error(err))

// For important issues that shouldn't crash production
logger.DPanic("unexpected state", tag.Error(err))

// For expected errors
logger.Warn("operation failed, will retry", tag.Error(err))

Wrapping Errors

Provide context when wrapping errors:
if err := updateDatabase(ctx, record); err != nil {
    return fmt.Errorf("update database for workflow %s: %w", workflowID, err)
}

Testing Best Practices

Write Tests for New Functionality

All new features require tests covering:
  • Best-case scenarios
  • Failure modes
  • Edge cases
  • Error handling

Test Organization

func TestFeatureName(t *testing.T) {
    t.Parallel() // Always use parallel unless there's a reason not to
    
    tv := testvars.New(t) // Use testvars for test data
    
    // Test setup
    s := testcore.NewEnv(t)
    
    // Test execution
    // ...
    
    // Assertions
    require.NoError(t, err) // Prefer require over assert
}

Prefer require Over assert

// Good: Stops test immediately on failure
require.NoError(t, err)
require.Equal(t, expected, actual)

// Avoid: Continues test after failure (can cause panics)
assert.NoError(t, err)
assert.Equal(t, expected, actual)

Avoid Testify Suites in Unit Tests

Testify suites are required for functional tests but should be avoided in unit tests:
// Good: Simple table-driven unit test
func TestValidation(t *testing.T) {
    t.Parallel()
    tests := []struct{
        name string
        input string
        want error
    }{
        // test cases
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            got := Validate(tt.input)
            require.Equal(t, tt.want, got)
        })
    }
}

// Functional tests can use testcore.NewEnv
func TestWorkflowExecution(t *testing.T) {
    s := testcore.NewEnv(t)
    // ...
}

Use require.Eventually Instead of time.Sleep

// Good: Wait for condition with timeout
require.Eventually(t, func() bool {
    return taskCompleted.Load()
}, 5*time.Second, 100*time.Millisecond)

// Bad: Hardcoded sleep (forbidden by linter)
time.Sleep(5 * time.Second)
require.True(t, taskCompleted.Load())

Run Tests After Changes

# Start with unit tests (fastest)
make unit-test

# Run relevant integration tests
go test -v -tags test_dep ./common/persistence/tests -run TestYourChange

# Run full test suite before submitting PR
make test

Dependency Management

Never Assume Libraries Are Available

Do not introduce new third-party libraries unless specifically requested or approved by maintainers.
Before using any library:
  1. Check if it exists in go.mod
  2. Look for existing usage patterns in the codebase
  3. Verify it’s appropriate for the use case

Updating Dependencies

# Update API dependencies
make update-go-api

# Update minor versions
make update-dependencies

# Check for major version updates
make update-dependencies-major

# Clean up
make gomodtidy

Code Generation

When to Regenerate Code

Regenerate code after modifying: Proto files:
make proto
Files with //go:generate directives:
make go-generate
Example //go:generate directive:
//go:generate mockgen -package mocks -destination mocks/interface.go . Interface

Verify Generated Code

After regenerating code:
# Check for uncommitted changes
git status

# Verify all code regenerated properly
make ensure-no-changes

Linting and Validation

Run Linters Before Committing

# Lint Go code
make lint-code

# Lint proto definitions
make lint-protos

# Lint GitHub Actions
make lint-actions

# Lint YAML files
make lint-yaml

# Run all linters
make lint

Fix Linting Issues

Many linting issues can be auto-fixed:
# Fix import formatting
make fmt-imports

# Fix YAML formatting
make fmt-yaml

# Apply go fix rules
make fmt-gofix

Breaking Changes

Verify proto changes don’t break compatibility:
make buf-breaking

License Headers

All source files require license headers. Verify:
make copyright

Commit Messages

Follow the Chris Beams guide for commit messages:
  1. Separate subject from body with a blank line
  2. Limit subject line to 50 characters
  3. Capitalize the subject line
  4. Do not end the subject line with a period
  5. Use imperative mood (“Add feature” not “Added feature”)
  6. Wrap body at 72 characters
  7. Use body to explain what and why, not how
Example:
Add workflow update validation

Validate update requests before processing to prevent invalid state
transitions. This fixes a race condition where concurrent updates
could corrupt workflow state.

Fixes #1234

Pull Request Titles

PR titles should:
  • Start with an uppercase letter
  • Have no period at the end
  • Not use generic titles like “bug fixes” or “updates”
  • Clearly describe the change
Good PR titles:
  • “Add validation for workflow update requests”
  • “Fix race condition in history service shutdown”
  • “Improve performance of task queue matching”
Bad PR titles:
  • “bug fixes”
  • “updates.”
  • “fix stuff”

Project Structure

Understand the project layout:
  • /api - Proto definitions and generated code
  • /chasm - Chasm library (Coordinated Heterogeneous Application State Machines)
  • /client - Client libraries for inter-service communication
  • /cmd - CLI commands and main applications
  • /common - Shared modules across all services
    • /common/dynamicconfig - Dynamic configuration
    • /common/membership - Cluster membership
    • /common/metrics - Metrics definitions
    • /common/namespace - Namespace cache
    • /common/persistence - Persistence layer
  • /components - Nexus components
  • /config - Configuration files
  • /proto - Internal proto definitions
  • /schema - Database schemas
  • /service - Main services
    • /service/frontend - Frontend service
    • /service/history - History service
    • /service/matching - Matching service
    • /service/worker - Worker service
  • /tests - Functional tests

Important Make Targets

Common Commands

# Build everything and run all checks
make all

# Install dependencies and build binaries
make install

# Build just the binaries
make bins

# Clean all build artifacts
make clean

Development Workflow

# 1. Make your changes

# 2. Format code
make fmt-imports

# 3. Regenerate if needed
make proto          # If you modified .proto files
make go-generate    # If you modified //go:generate files

# 4. Run tests
make unit-test

# 5. Run linters
make lint-code

# 6. Commit your changes
git add .
git commit -m "Your commit message"

CI Build Steps

The CI runs these checks:
make ci-build-misc
This includes:
  • Proto compilation
  • Code generation
  • Breaking change detection
  • Shell script checking
  • Import formatting
  • Go mod tidy
  • Verification that all generated code is committed

Debugging

Running Server Locally

Start the server with different persistence options:
# SQLite (default, no dependencies required)
make start

# SQLite with persistent file
make start-sqlite-file

# Cassandra + Elasticsearch
make start-dependencies
make install-schema-cass-es
make start-cass-es

# PostgreSQL
make start-dependencies
make install-schema-postgresql
make start-postgresql

# MySQL
make start-dependencies
make install-schema-mysql
make start-mysql

IDE Debugging (GoLand)

To debug the server:
  1. Set Run Type to “package”
  2. Package path: go.temporal.io/server/cmd/server
  3. Program arguments:
    --env development-sqlite --allow-no-auth start
    
For other databases:
--env development-postgres12 --allow-no-auth start
--env development-mysql8 --allow-no-auth start
--env development-cass-es --allow-no-auth start
See GoLand Debugging documentation for details.

Contributor License Agreement

All contributors must sign the Temporal Contributor License Agreement before we can merge changes.

Getting Help

Additional Resources

Build docs developers (and LLMs) love