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
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
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:
- Check if it exists in
go.mod
- Look for existing usage patterns in the codebase
- 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:
Files with //go:generate directives:
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:
All source files require license headers. Verify:
Commit Messages
Follow the Chris Beams guide for commit messages:
- Separate subject from body with a blank line
- Limit subject line to 50 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use imperative mood (“Add feature” not “Added feature”)
- Wrap body at 72 characters
- 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:
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:
- Set Run Type to “package”
- Package path:
go.temporal.io/server/cmd/server
- 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