Skip to content

Contributing Guide ​

Development workflow and contribution guidelines for Promenade Platform


Getting Started ​

Prerequisites ​

  • Go 1.24+ installed
  • Docker & Docker Compose (for PostgreSQL)
  • Make (comes with macOS/Linux)
  • Git configured
  • Code Editor: VS Code, GoLand, or Vim/Neovim

Fork and Clone ​

bash
# Fork repository on GitHub (click "Fork" button)

# Clone your fork
git clone https://github.com/YOUR_USERNAME/promenade.git
cd promenade

# Add upstream remote
git remote add upstream https://github.com/basilex/promenade.git

# Verify remotes
git remote -v

Setup Development Environment ​

bash
# Start PostgreSQL
make docker-up

# Run migrations
make migrate

# Run tests to verify setup
make test

# Start development server
make dev

Success: Server running on http://localhost:8081


Development Workflow ​

Branch Strategy ​

  • dev - Development branch (default, target for PRs)
  • main - Production-ready code (protected)
  • feature/* - New features
  • fix/* - Bug fixes
  • docs/* - Documentation updates

Creating a Feature ​

bash
# 1. Sync with upstream
git checkout dev
git pull upstream dev

# 2. Create feature branch
git checkout -b feature/my-awesome-feature

# 3. Make changes and commit
git add .
git commit -m "feat: add awesome feature"

# 4. Push to your fork
git push origin feature/my-awesome-feature

# 5. Open Pull Request on GitHub (to basilex/promenade:dev)

Project Structure ​

promenade/
 cmd/
    api/              # HTTP server entry point
    migrate/          # Migration CLI
 internal/
    contexts/         # Bounded Contexts (DDD)
       shared/      # Reference data
       identity/    # User management
       customer-mgmt/ # CRM
    infrastructure/   # Config, DB, etc.
 pkg/                  # Shared packages
    bus/             # Event Bus
    jwt/             # JWT auth
    logger/          # Logging
    uuidv7/          # UUID v7
 migrations/           # Database migrations
 test/                 # Three-tier tests
    smoke/           # Mock-based tests
    integration/     # DB tests
 docs/                 # Documentation

Coding Standards ​

Go Code Style ​

  1. Follow gofmt: Code must be formatted with go fmt
bash
# Format all code
make fmt

# Check formatting
go fmt ./...
  1. Follow golangci-lint: Pass all linter checks
bash
# Run linters
make lint

# Auto-fix some issues
golangci-lint run --fix
  1. Naming Conventions:
go
//  Interfaces: I prefix + PascalCase
type IUserRepository interface {}
type ICustomerUseCase interface {}

//  Implementations: lowercase private
type userRepository struct {}
type useCase struct {}

//  Constructors: New{Entity} or NewUseCase
func NewUserRepository(db *sqlx.DB) IUserRepository
func NewUseCase(repo IRepository) IUseCase

//  Methods: PascalCase
func (r *userRepository) Create(ctx context.Context, user *User) error

//  Constants: SCREAMING_SNAKE_CASE or PascalCase
const DefaultPageSize = 20
const UserStatusActive = "active"

//  Variables: camelCase
var userCount int
var isActive bool
  1. Error Handling:
go
//  Always handle errors
if err := repo.Create(ctx, user); err != nil {
    return fmt.Errorf("failed to create user: %w", err)
}

//  Use error wrapping
return fmt.Errorf("validation failed: %w", err)

//  Define domain errors
var ErrUserNotFound = errors.New("user not found")
var ErrEmailAlreadyExists = errors.New("email already exists")

//  Check specific errors
if errors.Is(err, ErrUserNotFound) {
    response.Error(c, http.StatusNotFound, "USER_NOT_FOUND", err.Error())
    return
}
  1. Context Propagation:
go
//  Always pass context as first parameter
func (uc *useCase) CreateUser(ctx context.Context, email, password string) (*User, error)

//  Use context for logging
logger.FromContext(ctx).Info("user created", slog.String("user_id", userID.String()))

//  Pass context to all repository calls
err := uc.repo.Create(ctx, user)

Testing Requirements ​

Three-Tier Testing Strategy ​

All code must have appropriate test coverage:

Test TypeLocationRequired Coverage
UnitSame directory as code85%+
Smoketest/smoke/contexts/All handlers
Integrationtest/integration/contexts/All repositories

Writing Tests ​

Unit Test (entity logic):

go
// internal/contexts/identity/user/entity_test.go
func TestUser_NewUser_ValidEmail(t *testing.T) {
    user, err := NewUser("test@example.com", "password123")
    
    assert.NoError(t, err)
    assert.NotNil(t, user)
    assert.NotEqual(t, uuidv7.Nil, user.ID)
}

Smoke Test (handler with mock):

go
// test/smoke/contexts/identity/user/handler_test.go
func TestUserHandler_Register_Returns201(t *testing.T) {
    gin.SetMode(gin.TestMode)
    mockUC := new(MockUserUseCase)
    handler := NewUserHandler(mockUC)
    
    mockUC.On("Register", mock.Anything, "test@example.com", "Test", "pass123").
        Return(&user.User{ID: uuidv7.New()}, nil).Once()
    
    // Test HTTP request...
    assert.Equal(t, http.StatusCreated, w.Code)
    mockUC.AssertExpectations(t)
}

Integration Test (repository with real DB):

go
// test/integration/contexts/identity/user/repository_test.go
func TestUserRepository_Create(t *testing.T) {
    db := integration.SetupTestDBWithCleanTables(t)
    repo := postgres.NewUserRepository(db.DB)
    ctx := context.Background()
    
    u, _ := user.NewUser("test@example.com", "password123")
    err := repo.Create(ctx, u)
    
    require.NoError(t, err)
    assert.NotEqual(t, uuidv7.Nil, u.ID)
}

Running Tests ​

bash
# All tests
make test

# By type
make test-unit
make test-smoke
make test-integration

# Specific context
go test ./internal/contexts/identity/... -v

# With coverage
make test-coverage

Before submitting PR: All tests must pass!


Commit Message Format ​

We follow Conventional Commits specification:

<type>(<scope>): <description>

[optional body]

[optional footer]

Types ​

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation only
  • style: Code style (formatting, missing semicolons, etc.)
  • refactor: Code refactoring (no functional changes)
  • test: Adding or updating tests
  • chore: Build process, dependencies, tooling
  • perf: Performance improvements

Examples ​

Feature:

feat(identity): add email verification for users

- Implement email verification endpoint
- Add verification token to User entity
- Send verification email via event bus

Closes #123

Bug Fix:

fix(customer): resolve duplicate email validation

- Add unique constraint on email per company
- Update validation logic in Customer entity

Fixes #456

Documentation:

docs(rbac): add comprehensive RBAC guide

- Document roles and permissions
- Add API endpoint examples
- Include JWT integration guide

Refactoring:

refactor(contact): extract value object creation logic

- Move Email, Phone, Address creation to factory methods
- Simplify Contact entity constructor

Pull Request Process ​

Before Submitting ​

  1. Sync with upstream:
bash
git checkout dev
git pull upstream dev
git checkout your-branch
git rebase dev
  1. Run all checks:
bash
make fmt      # Format code
make lint     # Pass linters
make test     # All tests pass
make build    # Builds successfully
  1. Update documentation (if applicable):
    • Update README.md if API changes
    • Add/update context README if new aggregate
    • Update docs/ if architecture changes

Submitting PR ​

  1. Push to your fork:
bash
git push origin your-branch
  1. Open Pull Request on GitHub:

    • Base: basilex/promenade:dev
    • Head: your-fork:your-branch
  2. Fill PR template:

markdown
## Description
Brief description of changes.

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing
- [ ] Unit tests added/updated
- [ ] Smoke tests added/updated
- [ ] Integration tests added/updated
- [ ] All tests pass locally

## Checklist
- [ ] Code follows project style guide
- [ ] Self-review completed
- [ ] Comments added for complex logic
- [ ] Documentation updated
- [ ] No new warnings/errors

Code Review Process ​

  1. Automated Checks: CI/CD runs tests, linters, build
  2. Maintainer Review: Code review by project maintainers
  3. Address Feedback: Make requested changes
  4. Approval: At least 1 approval required
  5. Merge: Maintainer merges to dev

Adding a New Aggregate ​

Step-by-step guide to add new aggregate in existing context:

1. Create Directory Structure ​

bash
mkdir -p internal/contexts/identity/newaggregate/adapter/http/handler/dto
mkdir -p internal/contexts/identity/newaggregate/adapter/repository/postgres

2. Create Entity ​

go
// internal/contexts/identity/newaggregate/entity.go
package newaggregate

import (
    "github.com/basilex/promenade/pkg/aggregate"
    "github.com/basilex/promenade/pkg/uuidv7"
)

type NewAggregate struct {
    aggregate.BaseAggregate
    
    ID   uuidv7.UUID
    Name string
    // ... other fields
}

// Factory method
func NewNewAggregate(name string) (*NewAggregate, error) {
    if name == "" {
        return nil, fmt.Errorf("name is required")
    }
    
    return &NewAggregate{
        BaseAggregate: aggregate.NewBase(),
        ID:            uuidv7.New(),
        Name:          name,
    }, nil
}

3. Create Repository Interface ​

go
// internal/contexts/identity/newaggregate/repository.go
package newaggregate

type IRepository interface {
    Create(ctx context.Context, agg *NewAggregate) error
    GetByID(ctx context.Context, id uuidv7.UUID) (*NewAggregate, error)
    Update(ctx context.Context, agg *NewAggregate) error
    Delete(ctx context.Context, id uuidv7.UUID) error
}

4. Create UseCase ​

go
// internal/contexts/identity/newaggregate/usecase.go
package newaggregate

type IUseCase interface {
    CreateNewAggregate(ctx context.Context, name string) (*NewAggregate, error)
    GetNewAggregate(ctx context.Context, id uuidv7.UUID) (*NewAggregate, error)
}

type useCase struct {
    repo IRepository
}

func NewUseCase(repo IRepository) IUseCase {
    return &useCase{repo: repo}
}

func (uc *useCase) CreateNewAggregate(ctx context.Context, name string) (*NewAggregate, error) {
    agg, err := NewNewAggregate(name)
    if err != nil {
        return nil, err
    }
    
    if err := uc.repo.Create(ctx, agg); err != nil {
        return nil, fmt.Errorf("failed to create aggregate: %w", err)
    }
    
    return agg, nil
}

5. Create Migration ​

bash
make migrate-new CONTEXT=identity NAME=add_newaggregate_table

Edit generated files in migrations/identity/

6. Write Tests ​

  • Unit tests: entity_test.go, usecase_test.go
  • Smoke tests: test/smoke/contexts/identity/newaggregate/handler_test.go
  • Integration tests: test/integration/contexts/identity/newaggregate/repository_test.go

7. Register in Router ​

go
// internal/contexts/identity/router.go
func NewRouter(db *sqlx.DB) *Router {
    // ... existing aggregates
    
    // New aggregate
    newAggRepo := newaggregateRepo.NewNewAggregateRepository(db)
    newAggUC := newaggregate.NewUseCase(newAggRepo)
    newAggHandler := newaggregateHTTP.NewNewAggregateHandler(newAggUC)
    
    return &Router{
        // ...
        newAggregateHandler: newAggHandler,
    }
}

func (r *Router) RegisterRoutes(api *gin.RouterGroup) {
    // ...
    
    newaggregates := identity.Group("/newaggregates")
    {
        newaggregates.POST("", r.newAggregateHandler.Create)
        newaggregates.GET("/:id", r.newAggregateHandler.GetByID)
    }
}

Documentation ​

Updating Documentation ​

When to update docs:

  • New API endpoint → Update API Reference
  • New aggregate → Update context README
  • Architecture change → Update docs/
  • New package → Add package README

Documentation locations:

  • README.md - Project overview
  • docs/ - Architecture guides
  • internal/contexts/*/README.md - Context documentation
  • pkg/*/README.md - Package documentation
  • website/ - VitePress documentation site

Building Documentation Site ​

bash
# Install dependencies
cd website
npm install

# Development server
npm run docs:dev

# Build static site
npm run docs:build

Common Issues ​

Tests Failing ​

Problem: Integration tests fail with duplicate key errors

Solution: Use SetupTestDBWithCleanTables(t) instead of SetupTestDB(t)

go
//  Wrong
db := integration.SetupTestDB(t)

//  Correct
db := integration.SetupTestDBWithCleanTables(t)

Linter Errors ​

Problem: golangci-lint reports errcheck errors

Solution: Handle all error returns

go
//  Wrong
db.Close()

//  Correct
defer func() {
    if err := db.Close(); err != nil {
        logger.Error("failed to close db", slog.Any("error", err))
    }
}()

Migration Issues ​

Problem: Migration fails with "relation already exists"

Solution: Reset database

bash
make db-reset
make migrate

Getting Help ​

Resources ​

Communication ​

  • GitHub Issues: Bug reports, feature requests
  • GitHub Discussions: Questions, ideas
  • Pull Requests: Code contributions
  • Email: alexander.vasilenko@gmail.com

Code of Conduct ​

Be respectful, professional, and constructive in all interactions.


License ​

By contributing, you agree that your contributions will be licensed under the MIT License.


Thank you for contributing to Promenade!

Built with Domain-Driven Design and Go