Refactor God Packages
Problem
God packages accumulate too many responsibilities, making code hard to understand, test, and maintain.
// ❌ God package - does everything
package app
// User management (50 files)
type User struct{} func CreateUser() {} func DeleteUser() {}
// Payment processing (30 files)
type Payment struct{} func ProcessPayment() {}
// Email (20 files)
type Email struct{} func SendEmail() {}
// Logging, Config, Database, HTTP handlers... (200+ files total)This guide shows practical steps for refactoring god packages into focused modules.
Solution Strategies
Identify Responsibilities
Map out what the package actually does before refactoring.
When to use: Before starting any refactoring work.
// ✅ Audit current package
// 1. List all types in the package
// 2. Group related types together
// 3. Identify dependencies between groups
// 4. Look for natural boundaries
// Current god package structure:
package app
// Group 1: User management
type User struct{}
type UserRepository interface{}
type UserService struct{}
func CreateUser() {}
func UpdateUser() {}
// Group 2: Authentication
type Session struct{}
type Token struct{}
func Login() {}
func Logout() {}
// Group 3: Payment
type Payment struct{}
type Invoice struct{}
func ProcessPayment() {}
func GenerateInvoice() {}
// Group 4: Notification
type Email struct{}
type SMS struct{}
func SendEmail() {}
func SendSMS() {}Create dependency diagram:
// Visualization (using comments or external tool)
// user -> auth (users need authentication)
// payment -> user (payments belong to users)
// notification -> user (send notifications to users)
// notification -> payment (payment confirmations)Extract Focused Sub-Packages
Move related types and functions into dedicated packages.
// ❌ Before - everything in app/
app/
user.go // 500 lines
payment.go // 400 lines
email.go // 300 lines
config.go // 200 lines
database.go // 350 lines
// ... 50 more files
// ✅ After - focused packages
user/
user.go // Domain model
repository.go // Data access
service.go // Business logic
auth/
session.go
token.go
service.go
payment/
payment.go
invoice.go
processor.go
repository.go
notification/
email.go
sms.go
sender.goStep-by-step extraction:
// Step 1: Create new package directory
mkdir -p user
// Step 2: Move related files
mv user.go user/
mv user_repository.go user/repository.go
mv user_service.go user/service.go
// Step 3: Update package declaration
// In user/user.go:
package user // Changed from package app
// Step 4: Fix imports in original package
// In app/main.go:
import "yourproject/user"
// Step 5: Update callers
u := user.CreateUser() // Was app.CreateUser()Handle Circular Dependencies
Break circular dependencies using interfaces and dependency injection.
Problem scenario:
// ❌ Circular dependency
package user
import "yourproject/payment"
type User struct {
Payments []*payment.Payment // user depends on payment
}
package payment
import "yourproject/user"
type Payment struct {
User *user.User // payment depends on user - circular!
}Solution 1: Define interface where it’s used
// ✅ User package defines interface
package user
type PaymentProvider interface {
FindByUser(userID string) ([]PaymentInfo, error)
}
type PaymentInfo struct {
ID string
Amount int
Status string
}
type Service struct {
payments PaymentProvider
}
func (s *Service) GetUserWithPayments(userID string) (*User, error) {
payments, err := s.payments.FindByUser(userID)
// Use payments...
}
// ✅ Payment package implements interface
package payment
import "yourproject/user"
type Repository struct {
db *sql.DB
}
// Implements user.PaymentProvider
func (r *Repository) FindByUser(userID string) ([]user.PaymentInfo, error) {
// Query database...
return []user.PaymentInfo{}, nil
}Solution 2: Extract shared types to separate package
// ✅ Create domain package for shared types
package domain
type UserID string
type PaymentID string
// Shared value objects
type Money struct {
Amount int
Currency string
}
// ✅ User package uses domain types
package user
import "yourproject/domain"
type User struct {
ID domain.UserID
}
// ✅ Payment package uses domain types
package payment
import "yourproject/domain"
type Payment struct {
ID domain.PaymentID
UserID domain.UserID // No circular dependency!
Amount domain.Money
}Solution 3: Use dependency injection
// ✅ Main package wires dependencies
package main
import (
"yourproject/user"
"yourproject/payment"
)
func main() {
// Create repositories
userRepo := user.NewRepository(db)
paymentRepo := payment.NewRepository(db)
// Inject dependencies
userService := user.NewService(userRepo, paymentRepo) // Pass payment repo
paymentService := payment.NewService(paymentRepo, userRepo) // Pass user repo
// No circular imports!
}Use Internal Packages for Shared Code
Use internal/ directory for code shared only within your project.
// ✅ Internal package structure
yourproject/
user/
user.go
service.go
payment/
payment.go
service.go
internal/
database/
connection.go // Shared database utilities
validation/
validator.go // Shared validation logic
errors/
errors.go // Shared error types
// ✅ Internal packages can be imported within project
package user
import "yourproject/internal/database"
import "yourproject/internal/validation"
// ❌ Cannot be imported from outside project
// External projects cannot import yourproject/internal/*Benefits:
- Clearly marks private implementation details
- Prevents external packages from depending on internals
- Allows refactoring internals without breaking external API
Progressive Refactoring Strategy
Refactor incrementally to minimize risk.
Phase 1: Extract domain types
// Week 1: Move just the domain models
package user
type User struct {
ID string
Email string
Name string
}
// Keep everything else in app/ for now
package app
import "yourproject/user"
func CreateUser(email, name string) (*user.User, error) {
// Business logic still here
}Phase 2: Extract repositories
// Week 2: Move data access
package user
type Repository interface {
Find(id string) (*User, error)
Save(user *User) error
}
type PostgresRepository struct {
db *sql.DB
}
// Business logic still in app/Phase 3: Extract services
// Week 3: Move business logic
package user
type Service struct {
repo Repository
}
func (s *Service) Create(email, name string) (*User, error) {
// Business logic here
}
// Update app/ to use service
package app
import "yourproject/user"
var userService *user.Service
func init() {
userService = user.NewService(userRepo)
}Phase 4: Update all callers
// Week 4: Migrate all call sites
// Old code (deprecated but still works)
user, err := app.CreateUser(email, name)
// New code
user, err := userService.Create(email, name)
// Mark old code as deprecated
// Deprecated: Use user.Service.Create instead
func CreateUser(email, name string) (*User, error) {
return userService.Create(email, name)
}Package Naming Conventions
Choose clear, focused package names.
// ❌ Vague names
package utils
package common
package helpers
package lib
package misc
// ✅ Specific names describing purpose
package user
package payment
package notification
package http
package postgres
// ✅ Package name should complete this sentence:
// "This package provides..."
// - user management
// - payment processing
// - notification delivery
// - HTTP server
// - PostgreSQL database accessAvoid Over-Nesting
Keep package hierarchy flat when possible.
// ❌ Over-nested packages
yourproject/
internal/
domain/
model/
entity/
user/
user.go // Too deep!
// ✅ Flat structure
yourproject/
user/
user.go
payment/
payment.go
// ✅ One level of nesting when needed
yourproject/
user/
user.go
internal/
validation.go // Implementation detailPutting It All Together
When you’re ready to refactor a god package, start by understanding what you have. List all types, functions, and files. Group related functionality together by drawing boxes around things that change together or represent the same business concept. User registration, login, and profile updates belong together. Payment processing, invoicing, and refunds form another group.
Create a dependency diagram showing how these groups relate. If user management needs authentication, draw an arrow. If payments need users, draw another arrow. Look for circular dependencies - they’ll force you to make architectural decisions before you can extract packages successfully.
Begin extraction with the groups that have fewest dependencies. Domain models often work well as a starting point - they’re referenced by everything but don’t depend on much. Move user types into a user package, payment types into a payment package. Update imports in the original package and verify tests still pass.
Handle circular dependencies by defining interfaces where they’re used rather than where they’re implemented. The user package can define a PaymentProvider interface without depending on the payment package. The payment package implements this interface. This inversion of dependencies breaks the cycle while maintaining flexibility.
Extract progressively rather than all at once. Move domain types first, then repositories, then services, then finally update all call sites. Each phase should leave the code in a working state. Keep deprecated wrapper functions in the original package temporarily to ease migration - remove them once all callers have migrated.
Common Mistakes to Avoid
Don’t create utility packages:
// ❌ Generic utility packages become new god packages
package utils
func FormatUser(user User) string {}
func ValidateEmail(email string) bool {}
func CalculateTotal(items []Item) int {}
func SendNotification(user User) error {}
// ✅ Put utilities with related functionality
package user
func (u User) Format() string {} // User formatting with users
func ValidateEmail(email string) bool {} // Email validation with users
package cart
func Total(items []Item) int {} // Total calculation with cartDon’t split packages by layer alone:
// ❌ Layer-based organization
models/
user.go
payment.go
order.go
services/
user_service.go
payment_service.go
order_service.go
repositories/
user_repository.go
payment_repository.go
order_repository.go
// ✅ Feature-based organization
user/
user.go
service.go
repository.go
payment/
payment.go
service.go
repository.go
order/
order.go
service.go
repository.goDon’t extract prematurely:
// ❌ Too many tiny packages
email/validator/validator.go // 20 lines
email/formatter/formatter.go // 15 lines
email/sender/sender.go // 30 lines
// ✅ One focused package
email/
email.go // Types
validator.go // Validation
formatter.go // Formatting
sender.go // SendingSummary
Refactoring god packages into focused modules requires systematic understanding of existing responsibilities before making changes. List all types and functions, group them by business concept, and map their dependencies. This analysis reveals natural boundaries where you can split the package and identifies circular dependencies you’ll need to break.
Extract progressively rather than attempting a big-bang refactoring. Start with domain types that have minimal dependencies, then move data access layers, followed by business logic. Each phase should leave the code in a working state with passing tests. Use deprecated wrappers temporarily to ease migration of call sites.
Break circular dependencies using interfaces defined where they’re used rather than where they’re implemented. This inverts the dependency direction and allows packages to depend on abstractions rather than concrete implementations. For shared types that create cycles, extract them to a separate domain package or rethink whether the dependency is necessary.
Organize by feature rather than by layer - user package contains user models, services, and repositories together rather than scattering them across separate model/service/repository packages. This keeps related code together and makes the codebase easier to navigate.
Keep package hierarchies relatively flat. Deep nesting like domain/model/entity/user creates navigation overhead without adding value. One or two levels of nesting with the internal directory for implementation details usually suffices.
Choose focused package names that describe what the package provides - user, payment, notification - rather than vague names like utils, common, or helpers. Generic packages tend to become new god packages over time as convenient dumping grounds for unrelated functionality.
These strategies work together to transform tangled god packages into well-organized, maintainable codebases where each package has a clear purpose and minimal dependencies.