Now I have the full picture. Here are concrete examples:
1. Auto-Register Domains
The Problem Today
Every time someone adds a new domain manager to ledger, they must:
- Add a field to
server.go (line ~140-170)
- Add an
initializeXxxManager() method to server_init.go
- Add a call to that method in
InitDomains() (line ~987-1075)
This means 3 locations in 2 files that everyone editing domains must touch. When two people add domains in parallel → merge conflict.
Current Pattern (server_init.go:1026-1032)
func InitDomains(ctx context.Context, s *Server, config *config.Config) error {
// ... 20+ other initializations ...
if err := s.initializeDelinquencyManager(); err != nil {
return err
}
if err := s.initializeCreditLineOverrideManager(); err != nil {
return err
}
// ... 10+ more initializations ...
}
Every new domain adds 3 lines here. Two PRs adding domains = guaranteed conflict.
Proposed Pattern: Self-Registering Domains
Step 1: Create a registry (new file: ledger/internal/server/registry.go)
package server
import "context"
// DomainInitializer is a function that initializes a domain and attaches it to the server.
type DomainInitializer struct {
Name string
Init func(ctx context.Context, s *Server) error
}
// domainRegistry holds all registered domain initializers.
// Domains register themselves via init() functions.
var domainRegistry []DomainInitializer
// RegisterDomain adds a domain initializer to the registry.
// Called from init() functions in each domain package.
func RegisterDomain(name string, init func(ctx context.Context, s *Server) error) {
domainRegistry = append(domainRegistry, DomainInitializer{Name: name, Init: init})
}
// InitAllDomains runs all registered domain initializers.
func InitAllDomains(ctx context.Context, s *Server) error {
for _, d := range domainRegistry {
if err := d.Init(ctx, s); err != nil {
return fmt.Errorf("failed to initialize domain %s: %w", d.Name, err)
}
}
return nil
}
Step 2: Each domain registers itself (e.g., ledger/internal/domains/delinquencymanager/register.go)
package delinquencymanager
import (
"context"
"github.com/go-backend/ledger/internal/server"
)
func init() {
server.RegisterDomain("delinquency", initDelinquencyManager)
}
func initDelinquencyManager(ctx context.Context, s *server.Server) error {
manager, err := NewManager(
WithLogger(s.Logger()),
WithStats(s.Stats()),
WithCreditLineDAO(s.SQLDao().CreditLineDAO),
WithTermsDAO(s.SQLDao().TermsDAO),
WithFeatureStore(s.FeatureStore()),
)
if err != nil {
return err
}
s.SetDelinquencyManager(manager)
return nil
}
Step 3: Replace InitDomains with the registry call
// server_init.go - InitDomains becomes:
func InitDomains(ctx context.Context, s *Server, config *config.Config) error {
return InitAllDomains(ctx, s)
}
Step 4: Import all domain packages in one place (ledger/internal/server/domains.go)
package server
// Import all domain packages to trigger their init() registration.
// Add new domains here - this is the ONLY file that needs editing.
import (
_ "github.com/go-backend/ledger/internal/domains/collectionmanager"
_ "github.com/go-backend/ledger/internal/domains/creditlineoverridemanager"
_ "github.com/go-backend/ledger/internal/domains/creditmanager"
_ "github.com/go-backend/ledger/internal/domains/creditprofilemanager"
_ "github.com/go-backend/ledger/internal/domains/delinquencymanager"
_ "github.com/go-backend/ledger/internal/domains/feeprofilemanager"
_ "github.com/go-backend/ledger/internal/domains/loanmanager"
// ... etc
)
Why This Is Better
| Aspect |
Before |
After |
| Files to edit for new domain |
2 (server.go, server_init.go) |
1 (domains.go import + new register.go) |
| Lines changed in shared files |
~15 lines across 2 files |
1 import line |
| Merge conflict probability |
High (sequential blocks) |
Low (import list is easy to merge) |
| Domain code locality |
Split across 3 locations |
All in domain package |
The key insight: import lists merge cleanly because git can auto-merge additions to different lines. Sequential if err := ...; return err blocks cannot.
2. Extract Generated Files from Git Merge Conflicts
The Problem Today
Generated files like z_mocks_*.go and z_swagger*.json are:
- Changed in 15+ commits out of the last 50
- Regenerated whenever their source changes
- Cause merge conflicts that are meaningless (just regenerate!)
Example: Two PRs both add a method to census.proto. Both regenerate z_mocks_census_grpc_client.go. Merge conflict. But the resolution is always: regenerate from merged proto.
Solution: .gitattributes Merge Strategy
Create .gitattributes in repo root:
# Generated mock files - always accept "ours" on merge, then regenerate
**/z_mock*.go merge=ours
**/z_mocks_*.go merge=ours
# Generated swagger files - same strategy
api/swagger/z_swagger*.json merge=ours
api/swagger/z_swagger*.yaml merge=ours
# Generated proto files
**/*.pb.go merge=ours
How merge=ours works:
When git encounters a conflict in these files during merge/rebase:
1. Instead of showing conflict markers, it keeps "our" version
2. The merge succeeds without manual intervention
3. CI then regenerates the files from source (proto, interfaces)
Required CI step (add to PR workflow):
# .github/workflows/pr-checks.yml
- name: Regenerate and verify generated files
run: |
# Regenerate mocks
go generate ./...
# Regenerate swagger
./scripts/generate-and-validate-swagger.sh
# Fail if there are uncommitted changes (generated files out of sync)
git diff --exit-code || (echo "Generated files out of sync. Run 'go generate ./...' and commit." && exit 1)
Alternative: Don't Commit Generated Files At All
A more aggressive approach: add generated files to .gitignore and generate them in CI/locally.
.gitignore additions:
# Generated mocks - regenerate with 'go generate ./...'
**/z_mock*.go
**/z_mocks_*.go
# Generated swagger - regenerate with './scripts/generate-and-validate-swagger.sh'
api/swagger/z_swagger*.json
api/swagger/z_swagger*.yaml
Makefile target for local dev:
.PHONY: generate
generate:
go generate ./...
./scripts/generate-and-validate-swagger.sh
.PHONY: setup
setup: generate
# Other setup steps...
Why This Is Better
| Approach |
Merge Conflicts |
Repo Size |
Dev Experience |
| Current (commit all) |
Frequent |
Larger (+1003 mock files) |
Simple but conflict-prone |
merge=ours + CI check |
None |
Same |
Auto-resolves, CI validates |
.gitignore generated |
None |
Smaller |
Must run make generate |
Recommendation: Start with merge=ours + CI validation. It's less disruptive than removing files from git entirely, but eliminates the merge conflict problem.
Concrete Before/After
Before (typical merge conflict in z_mocks_census_grpc_client.go):
<<<<<<< HEAD
func (_m *MockCensusClient) ListCreditLinesByDebtSale(ctx context.Context, ...) {
=======
func (_m *MockCensusClient) BulkUploadDebtSaleCreditLine(ctx context.Context, ...) {
>>>>>>> feature-branch
Developer must: understand both changes, manually merge, hope they got it right.
After (with merge=ours):
# Merge succeeds automatically
# CI runs go generate
# CI commits regenerated file (or fails if source is wrong)
No manual intervention. The generated file is always correct because it's regenerated from the merged source.
Migration Path
Week 1: Add .gitattributes for generated files
- Zero risk, immediate benefit
- Merge conflicts in
z_* files disappear
Week 2-3: Pilot domain registry in ledger
- Create
registry.go
- Migrate 2-3 domains to self-registration
- Keep old
InitDomains working for non-migrated domains
Week 4+: Roll out to other services
- Apply same pattern to
census, onboard, teller
- Each service can migrate incrementally