Naming Conventions
Standards every Code2b engineer follows when writing backend code, database schema, and HTTP APIs. When in doubt, match what enrichabl already does.
General principles
- Be explicit over clever. Names should read like documentation.
- One concept, one name. Do not alias the same entity as
user,profile, andaccountin different layers unless they are genuinely different domain objects. - Match the layer. Database uses
snake_case. Go exported identifiers usePascalCase. JSON field names follow the consumer (usuallysnake_casefor REST). - Prefix by entity in the database. Every column on a table is prefixed with that table's entity name. This avoids ambiguous
statusorcreated_atin joins. - Verify before you ship. Run
go build ./...,go vet, andgofmton Go changes. Migrations must be idempotent and wrapped in a transaction.
Database
Tables
| Rule | Example |
|---|---|
| Plural nouns | users, orders, enrichment_jobs |
snake_case, lowercase |
credit_wallets, api_keys |
| Junction / child tables name both sides | enrichment_job_leads, pipeline_leads |
CREATE TABLE IF NOT EXISTS enrichment_jobs (
enrichment_job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enrichment_job_user_id UUID NOT NULL REFERENCES profiles(profile_id),
enrichment_job_status TEXT NOT NULL DEFAULT 'PENDING',
enrichment_job_created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
enrichment_job_updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Columns
| Pattern | Example |
|---|---|
| Primary key | {entity}_id |
| Foreign key | {entity}_{referenced_entity}_id |
| Timestamps | {entity}_created_at, {entity}_updated_at |
| Status / type enums | {entity}_status, {entity}_type |
| Booleans | {entity}_is_active, {entity}_has_email |
| Counts / amounts | {entity}_credit_balance, {entity}_retry_count |
Child tables keep the full prefix: enrichment_job_lead_status, not bare status.
Enums and status values
Use SCREAMING_SNAKE_CASE string constants in the database and in Go:
const (
EnrichmentStatusPending = "PENDING"
EnrichmentStatusInProgress = "IN_PROGRESS"
EnrichmentStatusCompleted = "COMPLETED"
EnrichmentStatusFailed = "FAILED"
)Never store display labels in the DB. Store the machine value; format for humans in the UI.
Indexes and constraints
| Type | Pattern | Example |
|---|---|---|
| Index | idx_{table}_{columns} |
idx_enrichment_jobs_user_id |
| Unique | uq_{table}_{columns} |
uq_api_keys_key_hash |
| Check | chk_{table}_{rule} |
chk_service_pricing_unit_check |
| Foreign key | {table}_{column}_fkey |
Postgres default is fine |
Migrations
- File name:
migration-X.Y.Z.sqlor numbered001_create_users.sqlfor greenfield services. - Always wrap in
BEGIN;...COMMIT;. - Use idempotent DDL:
CREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS. - Bump the project
versiontable when the repo uses one. - Update the consolidated
schema.sqlafter every migration. - Apply by hand with
psql. We do not rely on auto-migration runners against production.
Go code
Packages and directories
Follow the DDD + hexagonal layout documented in Go: DDD + Hexagonal Architecture.
| Layer | Package role | Example path |
|---|---|---|
| Entry | main only wires dependencies |
cmd/api/main.go |
| Inbound | HTTP/gRPC handlers, middleware | internal/inbound/http/handler/ |
| Domain | Models, services, repository ports | internal/domain/user/ |
| Outbound | DB, cache, external APIs, adapters | internal/outbound/repository/ |
Package names: lowercase, single word, no underscores. Directory names may use underscores when the domain is multi-word (ai_column_template is acceptable; prefer one word when possible).
Files
| Rule | Example |
|---|---|
| One primary exported function per file in handler/usecase layers | startEnrichment.go, getJobByID.go |
| File name matches the main symbol, camelCase | user_handler.go, personalTokenAuth.go |
| Tests sit next to source | service_test.go |
No utils.go junk drawers |
Split by domain or delete |
Functions and methods
| Visibility | Style | Example |
|---|---|---|
| Exported | PascalCase verb phrase | StartEnrichment, GetUserByID, ValidateSubmission |
| Unexported | camelCase verb phrase | preflightCheck, mapRowToLead |
| Constructors | New or NewThing |
NewPostgresRepository, NewHandler |
| Interfaces (ports) | Noun or -er suffix |
UserRepository, EmailSender |
| Errors | Err + PascalCase |
ErrNotFound, ErrPipelineAccessDenied |
Receivers: short, typed, consistent within a file (s *Service, h *Handler, r *Repository).
Structs and types
// Domain model: mirrors DB where persisted
type EnrichmentJob struct {
ID uuid.UUID `json:"enrichment_job_id"`
UserID uuid.UUID `json:"enrichment_job_user_id"`
Status string `json:"enrichment_job_status"`
CreatedAt time.Time `json:"enrichment_job_created_at"`
}
// Request DTO: shorter JSON tags, no table prefix
type StartEnrichmentRequest struct {
PipelineID uuid.UUID `json:"pipeline_id"`
LeadIDs []uuid.UUID `json:"lead_ids"`
EnrichmentType string `json:"enrichment_type"`
}Persisted models mirror DB column names in JSON tags. Request/response DTOs use shorter, API-friendly names.
Context and errors
- Pass
context.Contextas the first parameter on anything that does I/O. - Wrap errors with context:
fmt.Errorf("get user %s: %w", id, err). - Return domain errors from services; translate to HTTP status in handlers only.
- Do not log and return the same error unless adding value.
HTTP API routes
Path style
| Rule | Example |
|---|---|
Prefix app routes with /api |
/api/enrichment/start |
Public token API under /v1 |
/v1/leads, /v1/enrichment/jobs |
| kebab-case path segments | /api/ai-columns/templates |
| Plural resource collections | /api/pipelines, /api/users |
| Nested resources | /api/pipelines/:id/leads/:lead_id |
| Health checks | /health or /healthz + /readyz |
Route parameters
Use :id for generic IDs. Use descriptive names when multiple IDs appear in one path: :job_id, :lead_id.
Handler naming
Register routes in one place (router.go). Handler methods match the action:
func (h *EnrichmentHandler) Start(c *fiber.Ctx) error { ... }
func (h *EnrichmentHandler) GetJob(c *fiber.Ctx) error { ... }
func (h *EnrichmentHandler) ListJobs(c *fiber.Ctx) error { ... }Response shape
Use consistent JSON envelopes where the project already does:
{
"data": { ... },
"error": null
}or plain resource objects for simple services. Do not mix styles within one service.
Frontend (when applicable)
| Item | Convention |
|---|---|
| React components | PascalCase file and export: UserTable.tsx |
| Hooks | use prefix: useEnrichmentJobs.ts |
| Route segments (App Router) | kebab-case folders: app/(dashboard)/enrichment-jobs/ |
| CSS | Tailwind utilities; shared tokens in design system |
| API client functions | camelCase: fetchEnrichmentJobs, createPipeline |
Git and PR hygiene
| Rule | Detail |
|---|---|
| Branch names | feat/short-description, fix/..., chore/... |
| One ticket per branch | Never stack unrelated work |
| Commits | Imperative subject, one logical change each |
| No AI attribution | No co-author trailers or generated-by footers |
| Never commit secrets | .env, keys, tokens stay local |
Checklist for new code
Before opening a PR:
- Table and column names follow entity-prefix
snake_case - Go exports are PascalCase; files are camelCase
- Repository interface lives in
domain/; implementation inoutbound/ - Handlers do not import SQL drivers or call raw SQL
- Migration is transactional and idempotent
-
go build ./...andgo vet ./...pass - API paths match existing service style (
/api/...vs bare REST)
When unsure, open enrichabl's backend/database/schema.sql or services/enrichment-service/ and copy the pattern.