userRepo is already there in UserService. So you inject MailService and add one method.
Then someone needs to handle subscriptions. The user is involved, so… UserService again. Then resumes. Then analytics. Then billing.
Before long, UserService has 12 injected dependencies and 400 lines covering six different domains. Nobody dares touch it.
That's a God Class — and your whole team is orbiting around it.
How to recognize one
- The constructor takes 8+ dependencies
- The class name is vague:
UserService,AppService,MainManager - Methods span completely different concerns — profile, billing, notifications, analytics
- Every other service in the app injects this one
- Any new feature gets added here because "everything's already available"
A real example
// ❌ God Class — UserService knows about everything
@Injectable()
export class UserService {
constructor(
private readonly userRepo: UserRepository,
private readonly jobRepo: JobRepository,
private readonly resumeRepo: ResumeRepository,
private readonly fileService: FileService,
private readonly mailer: MailService,
private readonly stripe: StripeService,
private readonly notificationService: NotificationService,
private readonly analyticsService: AnalyticsService,
private readonly cacheService: CacheService,
) {}
// User CRUD
async createUser(dto: CreateUserDto) { ... }
async updateUser(id: string, dto: UpdateUserDto) { ... }
async deleteUser(id: string) { ... }
// Profile
async uploadAvatar(userId: string, file: Express.Multer.File) { ... }
async updateProfileVisibility(userId: string, isPublic: boolean) { ... }
// Jobs (why is this here?)
async getUserAppliedJobs(userId: string) { ... }
async getSavedJobs(userId: string) { ... }
// Resumes (why is this here?)
async uploadResume(userId: string, file: Express.Multer.File) { ... }
async deleteResume(userId: string, resumeId: string) { ... }
// Billing (why is this here?)
async createSubscription(userId: string, planId: string) { ... }
async cancelSubscription(userId: string) { ... }
// Notifications (why is this here?)
async sendWelcomeEmail(userId: string) { ... }
async sendPasswordResetEmail(userId: string) { ... }
}sendWelcomeEmail alone requires mocking Stripe, the file service, three repos, and a cache. That's not a test — that's a setup ceremony.
The refactored version
// ✅ UserService — only user core logic
@Injectable()
export class UserService {
constructor(private readonly userRepo: UserRepository) {}
async createUser(dto: CreateUserDto) { ... }
async updateUser(id: string, dto: UpdateUserDto) { ... }
async deleteUser(id: string) { ... }
async findById(id: string) { ... }
}
// ✅ UserProfileService — avatar, visibility, profile settings
@Injectable()
export class UserProfileService {
constructor(
private readonly userRepo: UserRepository,
private readonly fileService: FileService,
) {}
async uploadAvatar(userId: string, file: Express.Multer.File) { ... }
async updateProfileVisibility(userId: string, isPublic: boolean) { ... }
}
// ✅ ResumeService — resume lifecycle
@Injectable()
export class ResumeService {
constructor(
private readonly resumeRepo: ResumeRepository,
private readonly fileService: FileService,
) {}
async uploadResume(userId: string, file: Express.Multer.File) { ... }
async deleteResume(userId: string, resumeId: string) { ... }
}
// ✅ SubscriptionService — billing, plans, Stripe
@Injectable()
export class SubscriptionService {
constructor(
private readonly stripe: StripeService,
private readonly userRepo: UserRepository,
) {}
async createSubscription(userId: string, planId: string) { ... }
async cancelSubscription(userId: string) { ... }
}Big class vs. God Class
- All methods belong to the same domain
- You can describe what it does in one sentence, without "and"
- Removing one external dependency doesn't make half the methods pointless
- It touches billing, email, file storage, and analytics at the same time
- Removing Stripe would break unrelated user profile methods
- Writing a test for one method requires setting up six mocks
Why it happens
// The thought process that creates God Classes:
//
// Sprint 1: UserService handles users. Makes sense.
// Sprint 2: Need to send email on register. userRepo is already here — inject MailService.
// Sprint 3: Resumes are user-related. Add it here.
// Sprint 4: Billing touches the user object. UserService again.
// Sprint 5: 'Why is this so hard to test?'