1. Introduction
A God Class is a class that knows too much, does too much, and controls too many parts of an application.
In Java enterprise applications, God Classes commonly appear in service-layer code where one class gradually becomes responsible for:
- Request validation
- Business rules
- Database operations
- DTO conversion
- External API calls
- Payment processing
- Notification handling
- Logging
- Error handling
- Audit creation
The class may initially look convenient because all related logic is available in one place. Over time, however, it becomes difficult to understand, test, change, and review safely.
During code review, a reviewer should not reject a class simply because it has many lines. The real question is whether the class owns too many unrelated responsibilities.
The goal is not to create dozens of tiny classes. The goal is to maintain clear responsibility boundaries so that each component has a focused reason to change.
2. What This Topic Means
Avoiding God Classes means designing Java classes so that one class does not become the central location for unrelated business and technical responsibilities.
Consider an OrderService that performs all of the following:
- Validates the order request
- Loads customer information
- Calculates prices
- Calculates discounts
- Reserves inventory
- Saves orders
- Charges the customer
- Sends confirmation emails
- Creates audit records
Although all these operations are related to order processing, they do not necessarily belong inside the same class.
Each responsibility has different reasons to change.
For example:
- Discount rules may change because of marketing requirements.
- Payment logic may change because the payment provider changes.
- Inventory logic may change because warehouse integration changes.
- Email logic may change because notification requirements change.
- Order persistence may change because of database changes.
When one class contains all these responsibilities, every business change increases the risk of affecting unrelated functionality.
A better design separates responsibilities while keeping the overall workflow easy to understand.
3. Why It Matters in Real Projects
Readability
A developer should be able to understand what a class is responsible for without reading hundreds or thousands of lines.
God Classes usually contain many unrelated methods, dependencies, helper functions, and conditional branches. This increases cognitive load.
Maintainability
Changes become risky when multiple features depend on the same large class.
A developer modifying payment logic may accidentally affect order validation or notification behavior.
Debugging
When production failures happen, God Classes make it harder to identify which responsibility caused the failure.
A stack trace pointing to a 1,500-line OrderService provides less useful information than one pointing to a focused PaymentProcessor.
Testability
Large classes usually require many mocked dependencies.
Tests often become difficult to arrange and understand because the class has too many collaborators.
Reliability
Highly coupled responsibilities increase regression risk.
A seemingly small modification can affect unrelated workflows.
Team Development
God Classes create merge conflicts because multiple developers frequently modify the same file.
For example:
- Developer A modifies payment logic.
- Developer B modifies discounts.
- Developer C modifies notification behavior.
All three developers may modify the same service class.
This slows development and makes Pull Requests harder to review.
4. Core Concept
The main problem behind a God Class is excessive responsibility concentration.
A healthy Java class should generally have a clear purpose.
For example:
OrderValidator
PricingService
InventoryService
PaymentService
OrderRepository
NotificationServiceEach class has a recognizable responsibility.
The coordinating business operation can still be represented through an orchestration service such as:
OrderProcessingServiceIts purpose is to coordinate the workflow rather than implement every detail itself.
For example:
validate order
calculate price
reserve inventory
save order
process payment
send confirmationThe orchestration remains visible, but individual business rules are delegated to focused components.
God Class Is Not Simply a Large Class
A large class is not automatically a God Class.
For example, a class may contain many related methods implementing one cohesive domain responsibility.
Similarly, a small class can still violate responsibility boundaries if it mixes unrelated concerns.
Reviewers should evaluate:
- Number of responsibilities
- Number of reasons to change
- Number of dependencies
- Cohesion between methods
- Ownership of business rules
- Coupling with infrastructure
- Difficulty of testing the class independently
5. Important Rules
- A class should have one clear primary responsibility.
- Group methods based on business responsibility, not merely convenience.
- Do not place unrelated helper methods into an existing service just because the service is already available.
- Separate validation when validation rules become substantial.
- Separate external system integration from core business logic.
- Separate notification logic from business transaction logic when possible.
- Keep persistence responsibilities inside repositories or dedicated persistence components.
- Avoid injecting a very large number of unrelated dependencies into one class.
- Use orchestration services to coordinate components instead of implementing all responsibilities directly.
- Do not extract classes only to reduce line count.
- Prefer meaningful domain boundaries over arbitrary class splitting.
- Keep transaction boundaries explicit when extracting database-related operations.
- Preserve business behavior during refactoring.
- Avoid circular dependencies between newly extracted services.
- Do not replace one God Class with several poorly designed utility classes.
6. Bad Code Example
Consider an e-commerce application containing the following service:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
private final ProductRepository productRepository;
private final PaymentGatewayClient paymentGatewayClient;
private final EmailClient emailClient;
private final AuditRepository auditRepository;
public OrderService(
OrderRepository orderRepository,
CustomerRepository customerRepository,
ProductRepository productRepository,
PaymentGatewayClient paymentGatewayClient,
EmailClient emailClient,
AuditRepository auditRepository) {
this.orderRepository = orderRepository;
this.customerRepository = customerRepository;
this.productRepository = productRepository;
this.paymentGatewayClient = paymentGatewayClient;
this.emailClient = emailClient;
this.auditRepository = auditRepository;
}
@Transactional
public Order createOrder(CreateOrderRequest request) {
if (request == null) {
throw new IllegalArgumentException("Order request cannot be null");
}
if (request.customerId() == null) {
throw new IllegalArgumentException("Customer id is required");
}
if (request.items() == null || request.items().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
Customer customer = customerRepository.findById(request.customerId())
.orElseThrow(() -> new CustomerNotFoundException(request.customerId()));
BigDecimal total = BigDecimal.ZERO;
for (OrderItemRequest itemRequest : request.items()) {
Product product = productRepository.findById(itemRequest.productId())
.orElseThrow(() -> new ProductNotFoundException(itemRequest.productId()));
if (product.getAvailableQuantity() < itemRequest.quantity()) {
throw new InsufficientInventoryException(product.getId());
}
BigDecimal lineTotal = product.getPrice()
.multiply(BigDecimal.valueOf(itemRequest.quantity()));
total = total.add(lineTotal);
product.setAvailableQuantity(
product.getAvailableQuantity() - itemRequest.quantity());
productRepository.save(product);
}
if (customer.isPremium()) {
total = total.multiply(new BigDecimal("0.90"));
}
PaymentRequest paymentRequest = new PaymentRequest(
customer.getId(),
total,
request.paymentToken());
PaymentResponse paymentResponse =
paymentGatewayClient.charge(paymentRequest);
if (!paymentResponse.successful()) {
throw new PaymentFailedException(paymentResponse.message());
}
Order order = new Order();
order.setCustomerId(customer.getId());
order.setTotalAmount(total);
order.setPaymentReference(paymentResponse.reference());
order.setStatus(OrderStatus.CONFIRMED);
Order savedOrder = orderRepository.save(order);
emailClient.sendOrderConfirmation(
customer.getEmail(),
savedOrder.getId(),
total);
AuditLog auditLog = new AuditLog();
auditLog.setAction("ORDER_CREATED");
auditLog.setEntityId(savedOrder.getId());
auditLog.setCreatedAt(Instant.now());
auditRepository.save(auditLog);
return savedOrder;
}
}This method may work, but the class has accumulated responsibilities belonging to multiple parts of the application.
7. Problems in the Bad Code
Too Many Responsibilities
OrderService performs:
- Input validation
- Customer retrieval
- Product retrieval
- Inventory validation
- Inventory modification
- Pricing
- Discount calculation
- Payment integration
- Order creation
- Persistence
- Email notification
- Audit logging
This makes the class highly coupled.
Low Cohesion
Many sections of the method depend on completely different collaborators.
Payment logic has little implementation-level relationship with email delivery.
Inventory modification has little relationship with audit persistence.
Difficult Unit Testing
Testing createOrder() requires mocking:
OrderRepositoryCustomerRepositoryProductRepositoryPaymentGatewayClientEmailClientAuditRepository
Additional responsibilities would increase that number further.
Database Calls Inside the Loop
The code executes:
productRepository.findById(...)and:
productRepository.save(...)for each item.
For large orders, this may generate unnecessary database round trips.
Mixed Business Logic and Infrastructure
The method combines:
- Business rules
- Persistence
- HTTP-style external integration
- Notification infrastructure
Changes to any one concern require modifying the same method.
Transaction Boundary Risk
The method is transactional but invokes an external payment gateway and email service.
A database transaction should generally not remain open unnecessarily while waiting for network calls.
More importantly, successful payment followed by database rollback requires explicit compensation or workflow design.
Notification Failure Risk
If email delivery throws an exception after payment and order persistence, transaction behavior may become confusing or incorrect depending on the exception and implementation.
Notification is usually not part of the core atomic order transaction.
Increased Regression Risk
A modification to discount calculation requires touching the same method responsible for inventory, payment, persistence, and notifications.
Difficult PR Review
A Pull Request containing changes to this method forces reviewers to reason about multiple domains simultaneously.
8. Code Review Findings
A senior reviewer should notice observations such as:
OrderServiceappears to own several independent responsibilities.- Validation rules are embedded directly inside orchestration logic.
- Pricing and premium discount calculation are mixed with persistence.
- Inventory is queried and updated item by item.
- External payment execution occurs inside the transaction-controlled service method.
- Email delivery is part of the synchronous order creation flow.
- Audit persistence is implemented directly in the order orchestration method.
- The service has many unrelated dependencies.
- Testing a single order scenario requires mocking most of the application's infrastructure.
- Adding another payment provider or discount type would probably make this method significantly larger.
- Transaction failure after successful external payment needs explicit consideration.
- Notification failure should not normally invalidate an otherwise successful order.
The reviewer should recommend extracting responsibilities based on behavior and ownership rather than simply saying that the method is too long.
9. Reviewer Comment Example
OrderServiceis currently handling validation, pricing, inventory, payment, persistence, notification, and auditing. Could we separate these responsibilities and keep this service focused on orchestrating the order workflow?
Another useful review comment:
The payment gateway call is executed inside the transactional workflow. Please review the transaction boundary because a remote call may remain pending while the database transaction is open, and payment success cannot automatically be rolled back with the database transaction.
Another example:
Product lookup and update are performed once per order item. Consider moving inventory reservation behind a dedicated component so we can optimize database access and test inventory behavior independently.
10. Improved Code
A more maintainable design separates the responsibilities.
Order Validator
@Component
public class OrderValidator {
public void validate(CreateOrderRequest request) {
if (request == null) {
throw new IllegalArgumentException("Order request cannot be null");
}
if (request.customerId() == null) {
throw new IllegalArgumentException("Customer id is required");
}
if (request.items() == null || request.items().isEmpty()) {
throw new IllegalArgumentException("Order must contain items");
}
if (request.items().stream().anyMatch(item -> item.quantity() <= 0)) {
throw new IllegalArgumentException("Item quantity must be greater than zero");
}
}
}Pricing Service
@Service
public class PricingService {
public BigDecimal calculateTotal(
Customer customer,
List<PricedOrderItem> items) {
BigDecimal total = items.stream()
.map(PricedOrderItem::lineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (customer.isPremium()) {
return total.multiply(new BigDecimal("0.90"));
}
return total;
}
}Inventory Service
@Service
public class InventoryService {
private final ProductRepository productRepository;
public InventoryService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<PricedOrderItem> reserve(
List<OrderItemRequest> requestedItems) {
List<Long> productIds = requestedItems.stream()
.map(OrderItemRequest::productId)
.distinct()
.toList();
Map<Long, Product> products = productRepository.findAllById(productIds)
.stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
List<PricedOrderItem> result = new ArrayList<>();
for (OrderItemRequest requestedItem : requestedItems) {
Product product = products.get(requestedItem.productId());
if (product == null) {
throw new ProductNotFoundException(requestedItem.productId());
}
if (product.getAvailableQuantity() < requestedItem.quantity()) {
throw new InsufficientInventoryException(product.getId());
}
product.setAvailableQuantity(
product.getAvailableQuantity() - requestedItem.quantity());
BigDecimal lineTotal = product.getPrice()
.multiply(BigDecimal.valueOf(requestedItem.quantity()));
result.add(new PricedOrderItem(
product.getId(),
requestedItem.quantity(),
product.getPrice(),
lineTotal));
}
productRepository.saveAll(products.values());
return result;
}
}Payment Service
@Service
public class PaymentService {
private final PaymentGatewayClient paymentGatewayClient;
public PaymentService(PaymentGatewayClient paymentGatewayClient) {
this.paymentGatewayClient = paymentGatewayClient;
}
public PaymentResult charge(
Long customerId,
BigDecimal amount,
String paymentToken) {
PaymentRequest request =
new PaymentRequest(customerId, amount, paymentToken);
PaymentResponse response = paymentGatewayClient.charge(request);
if (!response.successful()) {
throw new PaymentFailedException(response.message());
}
return new PaymentResult(response.reference());
}
}Notification Listener
@Component
public class OrderNotificationListener {
private final EmailClient emailClient;
public OrderNotificationListener(EmailClient emailClient) {
this.emailClient = emailClient;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(OrderCreatedEvent event) {
emailClient.sendOrderConfirmation(
event.customerEmail(),
event.orderId(),
event.totalAmount());
}
}Focused Order Processing Service
@Service
public class OrderProcessingService {
private final OrderValidator orderValidator;
private final CustomerRepository customerRepository;
private final InventoryService inventoryService;
private final PricingService pricingService;
private final PaymentService paymentService;
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
public OrderProcessingService(
OrderValidator orderValidator,
CustomerRepository customerRepository,
InventoryService inventoryService,
PricingService pricingService,
PaymentService paymentService,
OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher) {
this.orderValidator = orderValidator;
this.customerRepository = customerRepository;
this.inventoryService = inventoryService;
this.pricingService = pricingService;
this.paymentService = paymentService;
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public Order createOrder(CreateOrderRequest request) {
orderValidator.validate(request);
Customer customer = customerRepository.findById(request.customerId())
.orElseThrow(
() -> new CustomerNotFoundException(request.customerId()));
List<PricedOrderItem> items =
inventoryService.reserve(request.items());
BigDecimal total =
pricingService.calculateTotal(customer, items);
PaymentResult payment =
paymentService.charge(
customer.getId(),
total,
request.paymentToken());
Order order = new Order();
order.setCustomerId(customer.getId());
order.setTotalAmount(total);
order.setPaymentReference(payment.reference());
order.setStatus(OrderStatus.CONFIRMED);
Order savedOrder = orderRepository.save(order);
eventPublisher.publishEvent(
new OrderCreatedEvent(
savedOrder.getId(),
customer.getEmail(),
total));
return savedOrder;
}
}The exact transaction and payment architecture may need additional treatment in a production payment system, but responsibility boundaries are now substantially clearer.
11. Improved Code Explanation
Validation Has a Clear Owner
OrderValidator owns request-level validation.
The orchestration service no longer needs to know every validation rule.
New rules such as maximum order quantity can be added without increasing the complexity of the main workflow.
Pricing Has a Clear Owner
PricingService owns pricing calculations.
Discount changes no longer require modifications to inventory or payment code.
Inventory Logic Is Encapsulated
InventoryService owns:
- Product loading
- Stock validation
- Stock modification
- Inventory persistence
It can later implement:
- Optimistic locking
- Pessimistic locking
- Bulk updates
- Reservation expiration
without changing order orchestration.
Database Access Is Improved
Instead of retrieving every product individually, products are loaded using:
findAllById(productIds)The implementation can therefore reduce database round trips.
Payment Integration Is Encapsulated
PaymentService hides payment-gateway-specific behavior from order processing.
If the application changes payment provider, the impact is localized.
Notification Is Decoupled
The order service publishes an OrderCreatedEvent.
The notification listener reacts after transaction commit.
This helps prevent email logic from controlling the order transaction.
Orchestration Remains Visible
OrderProcessingService still clearly shows the business workflow.
The service has not been reduced to meaningless delegation. It provides value by coordinating the use case.
12. Bad Code vs Improved Code
| Area | Bad Code | Improved Code |
|---|---|---|
| Responsibility | One class owns almost everything | Responsibilities have clear owners |
| Readability | Large workflow mixed with implementation details | Main business flow is visible |
| Maintainability | Many unrelated changes affect one class | Changes are more localized |
| Testability | Many dependencies must be mocked | Components can be tested independently |
| Database Access | Product queries occur repeatedly | Bulk product retrieval is possible |
| Extensibility | New rules increase service complexity | Rules can evolve within focused components |
| PR Review | Reviewer must understand several domains at once | Changes are easier to isolate |
| Team Development | High chance of merge conflicts | Developers can work on separate components |
| Reliability | Failure boundaries are unclear | Responsibilities and failure handling are clearer |
13. Real Project Scenario
Consider a retail company with an order-management microservice.
Initially the application supports:
- Standard customers
- Credit-card payment
- One warehouse
- Email confirmation
The OrderService may initially remain manageable.
After two years, requirements expand.
The system now supports:
- Premium customer discounts
- Coupon codes
- Corporate pricing
- Multiple warehouses
- Partial inventory reservations
- Credit cards
- UPI
- Wallet payments
- Retryable payment failures
- Email notifications
- SMS notifications
- Push notifications
- Fraud checks
- Audit requirements
If all these features continue to be implemented inside the original OrderService, the service can become thousands of lines long.
Every feature team begins modifying the same class.
A payment-related Pull Request might accidentally affect:
- Inventory rollback
- Discount calculation
- Notification behavior
- Transaction handling
Separating these responsibilities before the service becomes unmanageable significantly reduces long-term maintenance cost.
14. Production Impact
A God Class does not normally cause a production outage simply because it is large.
The risk comes from excessive coupling and unclear responsibility boundaries.
Real production consequences may include:
Higher Regression Risk
A change intended for one workflow may affect unrelated behavior.
Difficult Incident Diagnosis
Developers may need to inspect a large amount of code to determine whether a failure originated in:
- Validation
- Database logic
- External integration
- Business calculations
- Notifications
Longer Deployment Cycles
Teams may become reluctant to modify a critical God Class because many workflows depend on it.
Merge Conflicts
Multiple feature teams modifying the same class can create frequent conflicts.
Transaction Problems
When database operations and network integrations are mixed together, transaction boundaries become difficult to reason about.
Performance Problems
A God Class may hide inefficient operations such as:
- Database calls inside loops
- Repeated API calls
- Repeated object transformation
- Large in-memory processing
The God Class itself is not a performance problem, but its complexity makes performance problems easier to introduce and harder to detect.
15. Common Developer Mistakes
Using One Service for the Entire Feature
A developer creates:
CustomerServiceand gradually adds:
- Registration
- Authentication
- Address management
- Loyalty points
- Notifications
- Reporting
- Audit operations
because all operations involve customers.
Domain relation alone does not mean all responsibilities belong in one class.
Extracting Only Private Methods
Developers sometimes turn a 500-line public method into twenty private methods and assume the God Class problem has been solved.
The code may become easier to read, but responsibility concentration remains.
Creating Generic Utility Classes
Another common mistake is moving unrelated logic into:
OrderUtils
CommonUtils
ApplicationHelperThis relocates the problem rather than solving it.
Splitting Classes Only by Line Count
A 300-line cohesive class may be healthier than five 60-line classes with unclear boundaries.
Over-Extracting
Creating a class for every five lines of code creates unnecessary indirection.
Refactoring should follow responsibilities, not arbitrary size rules.
Circular Service Dependencies
Poor extraction may produce:
OrderService -> PaymentService
PaymentService -> OrderServiceThis usually indicates unclear ownership.
Ignoring Transaction Boundaries
Moving methods to another Spring bean may change transactional behavior because Spring transactions commonly depend on proxy interception.
Keeping Shared Mutable State
Extracted singleton Spring services should normally remain stateless.
Shared mutable fields can introduce concurrency problems.
16. Edge Cases
Null or Invalid Requests
Validation must remain explicit after refactoring.
Moving responsibilities should not accidentally remove validation behavior.
Empty Order
The extracted validator should reject orders without items.
Duplicate Product IDs
If the same product appears more than once in an order, inventory logic must define whether quantities are aggregated or processed separately.
Concurrent Inventory Updates
Two customers may order the final available item simultaneously.
Class decomposition alone does not solve this problem.
The inventory component may require:
- Optimistic locking
- Pessimistic locking
- Atomic database updates
- Reservation-based design
External Payment Failure
The workflow must clearly define what happens when payment fails after inventory reservation.
Database Failure After Payment
This is especially important.
An external payment cannot be rolled back automatically through a local database transaction.
Production payment workflows may require:
- Idempotency
- Compensation
- Payment authorization before capture
- Saga-style orchestration
- Reliable messaging
Notification Failure
An email failure should generally not cause the completed order transaction to disappear.
Large Orders
Loading and updating hundreds or thousands of items may require optimized bulk operations.
17. Performance Considerations
Avoiding a God Class is primarily a design and maintainability concern rather than a direct performance optimization.
However, decomposition can expose performance boundaries more clearly.
Database Calls
In the bad implementation, product data is loaded inside a loop.
For n order items, this may result in approximately n product queries and multiple update calls.
Bulk operations can significantly reduce database round trips.
External API Calls
Payment APIs should be clearly isolated because they introduce network latency.
Reviewers can more easily identify:
- Timeouts
- Retry policies
- Circuit breakers
- Idempotency requirements
when the integration is encapsulated.
Transaction Duration
Long database transactions should not remain open unnecessarily while waiting for remote systems.
Memory Usage
Splitting a class into focused services usually has negligible memory impact.
Spring singleton bean objects themselves are normally insignificant compared with database, network, cache, and collection-processing costs.
Do Not Optimize Class Count
Avoiding God Classes should not become an exercise in minimizing object creation.
Architectural clarity is usually more important than the tiny cost of a few additional singleton service instances.
18. Security Considerations
A God Class is not automatically a security vulnerability.
However, mixed responsibilities can make security checks easier to forget or bypass.
Authorization
If authorization logic is scattered throughout a large service, newly added operations may accidentally bypass required permission checks.
Sensitive Data
Payment tokens, customer information, or authentication data should not be logged casually.
A large service handling many concerns increases the chance of sensitive values leaking into debug logs.
Input Validation
Validation responsibilities should remain explicit and consistently enforced.
External Integration Credentials
Payment or API credentials should remain in secure configuration and dedicated integration components rather than becoming mixed into business services.
Error Exposure
External provider errors may contain sensitive or internal information.
The application should not expose raw provider responses directly to API consumers.
Auditability
Dedicated auditing infrastructure can make security-sensitive operations easier to track consistently.
19. Testing Considerations
Refactoring a God Class should improve testing.
OrderValidator Tests
Positive cases:
- Valid customer ID
- Valid item list
- Positive quantities
Negative cases:
- Null request
- Missing customer ID
- Empty item list
- Zero quantity
- Negative quantity
PricingService Tests
Test:
- Standard customer pricing
- Premium customer discount
- Multiple items
- Decimal values
- Zero-value scenarios if allowed
InventoryService Tests
Test:
- Product exists
- Product does not exist
- Enough inventory
- Insufficient inventory
- Duplicate product requests
- Concurrent update behavior where applicable
PaymentService Tests
Test:
- Successful payment
- Declined payment
- Timeout
- Gateway exception
- Invalid response
OrderProcessingService Tests
The orchestration test should verify the workflow rather than re-testing every implementation detail.
For example:
@ExtendWith(MockitoExtension.class)
class OrderProcessingServiceTest {
@Mock
private OrderValidator orderValidator;
@Mock
private CustomerRepository customerRepository;
@Mock
private InventoryService inventoryService;
@Mock
private PricingService pricingService;
@Mock
private PaymentService paymentService;
@Mock
private OrderRepository orderRepository;
@Mock
private ApplicationEventPublisher eventPublisher;
@InjectMocks
private OrderProcessingService orderProcessingService;
@Test
void shouldCreateOrderSuccessfully() {
CreateOrderRequest request = TestData.validOrderRequest();
Customer customer = TestData.standardCustomer();
List<PricedOrderItem> items = TestData.pricedItems();
BigDecimal total = new BigDecimal("2500.00");
PaymentResult payment = new PaymentResult("PAY-101");
Order savedOrder = TestData.savedOrder();
when(customerRepository.findById(request.customerId()))
.thenReturn(Optional.of(customer));
when(inventoryService.reserve(request.items()))
.thenReturn(items);
when(pricingService.calculateTotal(customer, items))
.thenReturn(total);
when(paymentService.charge(
customer.getId(),
total,
request.paymentToken()))
.thenReturn(payment);
when(orderRepository.save(any(Order.class)))
.thenReturn(savedOrder);
Order result = orderProcessingService.createOrder(request);
assertEquals(savedOrder.getId(), result.getId());
verify(orderValidator).validate(request);
verify(inventoryService).reserve(request.items());
verify(paymentService).charge(
customer.getId(),
total,
request.paymentToken());
verify(eventPublisher).publishEvent(any(OrderCreatedEvent.class));
}
}Integration Tests
Integration tests should verify important boundaries such as:
- Database transaction behavior
- Repository operations
- Event publication
- Payment adapter behavior using a stub or test server
- Notification behavior after successful commit
20. Refactoring Guidelines
Refactoring a God Class should be incremental.
Step 1: Protect Existing Behavior
Before major refactoring, establish tests around critical existing workflows.
Do not start by moving hundreds of lines without behavioral protection.
Step 2: Identify Responsibilities
Group methods and code sections into meaningful concerns.
For example:
- Validation
- Pricing
- Inventory
- Payment
- Notification
- Persistence
Step 3: Identify Stable Boundaries
Determine which responsibilities can change independently.
Payment integration is usually an obvious candidate because it interacts with an external system.
Step 4: Extract One Responsibility
Move one cohesive responsibility into its own component.
Avoid extracting everything in a single Pull Request unless the codebase and test coverage make that safe.
Step 5: Delegate From the Existing Service
Keep the existing public API initially if other components depend on it.
This reduces migration risk.
Step 6: Verify Transaction Behavior
When moving methods between Spring components, confirm:
- Which method owns
@Transactional - Whether propagation behavior changed
- Whether self-invocation previously affected transactions
- Whether external calls remain inside database transactions
Step 7: Run Existing Tests
Verify that behavior remains unchanged.
Step 8: Add Focused Tests
After extraction, test the new component independently.
Step 9: Remove Dead Logic
Once migration is complete, remove duplicated or obsolete methods.
Step 10: Review Dependency Direction
Ensure extracted classes do not create circular dependencies.
21. Best Practices
Keep Orchestration Separate From Implementation Details
A use-case service should make the workflow understandable.
Example:
validate
reserve
calculate
pay
persist
publishThe orchestration should not contain every algorithm and infrastructure detail.
Design Around Business Capabilities
Prefer:
PricingService
InventoryService
PaymentServiceover vague classes such as:
OrderHelper
CommonService
UtilityManagerKeep Integration Logic Behind Dedicated Interfaces
For example:
public interface PaymentGateway {
PaymentResult charge(PaymentCommand command);
}The business layer should not depend heavily on provider-specific HTTP details.
Keep Spring Services Stateless
Avoid mutable per-request data inside singleton service fields.
Keep Public APIs Small
A service exposing twenty unrelated public methods may indicate poor cohesion.
Review Constructor Dependencies
A rapidly growing constructor can be a useful design warning.
It is not an automatic violation, but it should trigger review.
Extract Behavior, Not Just Code
A good extracted class owns a meaningful responsibility and can be understood independently.
22. Practices to Avoid
One Service Per Entity With Unlimited Responsibilities
Avoid treating an entity name as permission to place every related operation into one service.
Excessive Private Method Extraction
Private methods improve readability but do not automatically improve responsibility separation.
Generic Manager Classes
Examples:
OrderManager
ApplicationManager
ProcessManagerThese names often hide unclear responsibility boundaries.
Generic Helper Classes
Avoid dumping domain logic into:
CommonHelper
Utils
SharedUtilsStatic Business Logic Everywhere
Static utility methods make dependency injection, testing, and substitution harder when used for substantial business behavior.
Circular Dependencies
Do not solve decomposition by creating components that depend on each other in both directions.
Excessive Interface Creation
Not every class requires an interface.
Introduce interfaces when they provide useful abstraction boundaries, multiple implementations, integration boundaries, or testability benefits.
Micro-Class Explosion
Do not create a class for every few lines simply to claim compliance with SRP.
Good design optimizes cohesion and changeability, not number of classes.
23. Code Review Checklist
- Does this class have one clear primary responsibility?
- Can the responsibility of the class be explained in one concise sentence?
- Does the class contain business logic belonging to unrelated domains?
- Does the class mix validation, persistence, external integration, and notification logic?
- Does the class have an unusually large number of dependencies?
- Are some dependencies used only by a small subset of methods?
- Would a change to one business rule require touching unrelated code?
- Are there cohesive groups of methods that should belong to another component?
- Is the class acting as both orchestrator and detailed implementation?
- Are database operations hidden inside loops?
- Are remote API calls mixed with database transactions?
- Is notification behavior tightly coupled to the main business transaction?
- Would focused components be easier to unit test?
- Are extracted components named according to business responsibilities?
- Would extraction create circular dependencies?
- Are transaction boundaries still correct after refactoring?
- Is the proposed decomposition improving cohesion rather than simply reducing line count?
- Can multiple developers modify separate responsibilities without repeatedly changing the same file?
- Are critical business rules located in clear, discoverable components?
- Is the design becoming unnecessarily fragmented or over-engineered?
24. Common Pull Request Review Comments
- > This service currently owns validation, persistence, pricing, and notification logic. Could we extract the independent responsibilities and leave this class focused on orchestration?
- > The discount calculation appears to be a separate business responsibility. Moving it behind a pricing component would make the rule easier to test and change independently.
- > We are calling the repository once for every item. Could the inventory component load these products in a batch instead?
- > This external API call currently executes inside the transactional method. Please verify whether we need the database transaction to remain open during the remote request.
- > Notification failure should probably not roll back the completed business transaction. Consider publishing an event after the successful commit.
- > This class has dependencies for payment, email, inventory, reporting, and auditing. That suggests it may be coordinating too many unrelated concerns.
- > Extracting private methods improves readability, but the class would still own all responsibilities. I suggest moving the payment behavior into a dedicated component.
- > Please avoid moving this logic to
CommonUtils. The code represents domain behavior and deserves a domain-specific owner.
- > Before extracting this code, please preserve the existing transaction semantics and add tests for the current failure cases.
- > The new component should remain focused on inventory behavior. Adding payment rollback logic there would create another mixed-responsibility service.
25. Code Review Exercise
Review the following service as if it appeared in a Pull Request.
Identify:
- Responsibilities owned by the class
- Code smells
- Maintainability problems
- Database problems
- Failure-handling risks
- Testing problems
- Potential refactoring opportunities
@Service public class EmployeeService { private final EmployeeRepository employeeRepository; private final DepartmentRepository departmentRepository; private final PayrollClient payrollClient; private final EmailClient emailClient; private final AuditRepository auditRepository; public EmployeeService( EmployeeRepository employeeRepository, DepartmentRepository departmentRepository, PayrollClient payrollClient, EmailClient emailClient, AuditRepository auditRepository) { this.employeeRepository = employeeRepository; this.departmentRepository = departmentRepository; this.payrollClient = payrollClient; this.emailClient = emailClient; this.auditRepository = auditRepository; } @Transactional public Employee onboardEmployee(EmployeeRequest request) { if (request.name() == null || request.name().isBlank()) { throw new IllegalArgumentException("Name is required"); } if (request.email() == null || !request.email().contains("@")) { throw new IllegalArgumentException("Invalid email"); } Department department = departmentRepository.findById(request.departmentId()) .orElseThrow( () -> new IllegalArgumentException( "Department not found")); if (employeeRepository.existsByEmail(request.email())) { throw new IllegalArgumentException( "Employee email already exists"); } Employee employee = new Employee(); employee.setName(request.name()); employee.setEmail(request.email()); employee.setDepartmentId(department.getId()); employee.setStatus(EmployeeStatus.ACTIVE); Employee savedEmployee = employeeRepository.save(employee); PayrollResponse payrollResponse = payrollClient.createEmployee( savedEmployee.getId(), request.salary()); if (!payrollResponse.success()) { throw new IllegalStateException( "Payroll registration failed"); } emailClient.send( savedEmployee.getEmail(), "Welcome to the company"); AuditLog audit = new AuditLog(); audit.setAction("EMPLOYEE_CREATED"); audit.setEntityId(savedEmployee.getId()); auditRepository.save(audit); return savedEmployee; } }
Review this code before reading the solution.
26. Exercise Solution
The class owns several responsibilities:
- Input validation
- Department lookup
- Duplicate employee validation
- Employee persistence
- Payroll integration
- Welcome email delivery
- Audit creation
Problem 1: Validation Is Embedded in Orchestration
The service manually validates names and email addresses.
If onboarding validation expands, the method will continue growing.
A dedicated validator can provide a clear owner.
Problem 2: External Payroll Call Runs Inside the Transaction
payrollClient.createEmployee() is a remote operation.
If payroll succeeds but a later database operation fails, the external payroll system cannot automatically participate in the database rollback.
The workflow therefore needs explicit failure semantics.
Problem 3: Email Delivery Is Synchronous
A welcome email failure can potentially interfere with employee onboarding.
Email notification should usually happen after successful transaction completion.
Problem 4: Auditing Is Implemented Directly
Audit creation is another responsibility inside the orchestration service.
If multiple employee operations require auditing, a dedicated audit mechanism is preferable.
Problem 5: Service Is Becoming Difficult to Test
The onboarding test must coordinate multiple repositories and external systems.
As onboarding grows, the test setup will become increasingly complex.
Improved Validator
@Component
public class EmployeeOnboardingValidator {
private final EmployeeRepository employeeRepository;
public EmployeeOnboardingValidator(
EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
public void validate(EmployeeRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request is required");
}
if (request.name() == null || request.name().isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (request.email() == null || request.email().isBlank()) {
throw new IllegalArgumentException("Email is required");
}
if (employeeRepository.existsByEmail(request.email())) {
throw new DuplicateEmployeeEmailException(request.email());
}
}
}For robust email validation, the application should use its established validation mechanism rather than relying on a basic string check.
Payroll Service
@Service
public class PayrollService {
private final PayrollClient payrollClient;
public PayrollService(PayrollClient payrollClient) {
this.payrollClient = payrollClient;
}
public void register(Employee employee, BigDecimal salary) {
PayrollResponse response =
payrollClient.createEmployee(
employee.getId(),
salary);
if (!response.success()) {
throw new PayrollRegistrationException(employee.getId());
}
}
}Focused Onboarding Service
@Service
public class EmployeeOnboardingService {
private final EmployeeOnboardingValidator validator;
private final EmployeeRepository employeeRepository;
private final DepartmentRepository departmentRepository;
private final PayrollService payrollService;
private final ApplicationEventPublisher eventPublisher;
public EmployeeOnboardingService(
EmployeeOnboardingValidator validator,
EmployeeRepository employeeRepository,
DepartmentRepository departmentRepository,
PayrollService payrollService,
ApplicationEventPublisher eventPublisher) {
this.validator = validator;
this.employeeRepository = employeeRepository;
this.departmentRepository = departmentRepository;
this.payrollService = payrollService;
this.eventPublisher = eventPublisher;
}
@Transactional
public Employee onboard(EmployeeRequest request) {
validator.validate(request);
Department department =
departmentRepository.findById(request.departmentId())
.orElseThrow(
() -> new DepartmentNotFoundException(
request.departmentId()));
Employee employee = new Employee();
employee.setName(request.name());
employee.setEmail(request.email());
employee.setDepartmentId(department.getId());
employee.setStatus(EmployeeStatus.ACTIVE);
Employee savedEmployee = employeeRepository.save(employee);
payrollService.register(savedEmployee, request.salary());
eventPublisher.publishEvent(
new EmployeeOnboardedEvent(
savedEmployee.getId(),
savedEmployee.getEmail()));
return savedEmployee;
}
}Notification Listener
@Component
public class EmployeeNotificationListener {
private final EmailClient emailClient;
public EmployeeNotificationListener(EmailClient emailClient) {
this.emailClient = emailClient;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(EmployeeOnboardedEvent event) {
emailClient.send(
event.email(),
"Welcome to the company");
}
}Why These Changes Are Useful
- Validation is independently testable.
- Payroll integration has a dedicated owner.
- Notification behavior is separated from core onboarding.
- Main onboarding workflow is easier to understand.
- Future payroll-provider changes are localized.
- The service no longer contains every implementation detail.
However, the payroll/database consistency problem still requires an explicit business strategy.
For high-reliability systems, teams should consider patterns such as:
- Idempotent remote operations
- Pending employee states
- Retry workflows
- Compensation
- Reliable messaging
- Saga orchestration
Class extraction alone does not solve distributed transaction consistency.
27. Interview Perspective
God Classes frequently appear in senior Java, Spring Boot, system design, and code-review interviews.
The interviewer may not directly ask:
"What is a God Class?"
Instead, you may receive a scenario.
For example:
We have a 2,000-line Spring Boot service with 15 injected dependencies that validates requests, accesses multiple repositories, calls external APIs, sends emails, and generates reports. How would you improve it?
A strong answer should discuss:
- Identifying responsibilities
- Measuring cohesion
- Understanding reasons to change
- Protecting behavior with tests
- Extracting components incrementally
- Maintaining transaction boundaries
- Avoiding circular dependencies
- Separating infrastructure from business logic
- Avoiding over-engineering
- Keeping orchestration understandable
Senior-level discussions may also involve:
- Distributed transactions
- Event-driven notification
- Payment compensation
- Retry behavior
- Idempotency
- Domain boundaries
- Hexagonal architecture
- Dependency direction
The interviewer is usually interested in your ability to improve production code safely rather than your ability to quote the Single Responsibility Principle.
28. Interview Questions and Answers
Basic Question
Question: What is a God Class in Java?
Answer:
A God Class is a class that owns too many responsibilities and knows about too many parts of the application.
For example, a service that handles validation, database access, payment integration, email notification, report generation, and auditing is likely to have poor cohesion.
The problem is not simply class length. The real issue is excessive responsibility concentration and coupling.
Intermediate Question
Question: How do you identify a God Class during code review?
Answer:
I look for indicators such as:
- Many unrelated dependencies
- Many different reasons for the class to change
- Methods operating on unrelated concerns
- Business logic mixed with infrastructure
- Very complex test setup
- Large conditional workflows
- Frequent changes by different feature teams
- Poor cohesion between methods
I would not classify a class as a God Class based only on line count.
Advanced Question
Question: How would you refactor a God Class safely in a production Spring Boot application?
Answer:
I would first protect existing behavior with tests.
Then I would identify cohesive responsibilities such as validation, pricing, inventory, payment, or notification.
I would extract one responsibility at a time and initially delegate from the existing service to reduce migration risk.
I would verify transaction boundaries carefully because moving methods between Spring beans can change proxy-based transactional behavior.
I would also check:
- Failure handling
- Dependency direction
- Circular dependencies
- Integration behavior
- Performance
- Existing API contracts
I would avoid a large rewrite unless there were strong reasons and sufficient test coverage.
Scenario-Based Question
Question: An OrderService has 18 dependencies. Is that enough to prove it is a God Class?
Answer:
No.
A high dependency count is a warning signal, not proof.
I would inspect whether those dependencies support one cohesive orchestration responsibility or several unrelated responsibilities.
For example, a workflow coordinator may legitimately require several collaborators.
However, if only specific groups of methods use specific dependencies and the class changes for unrelated business reasons, decomposition is probably appropriate.
Code-Review Question
Question: What review comment would you leave on a 1,500-line service containing validation, payment, email, and database code?
Answer:
I would avoid writing only:
"This class is too large."
Instead, I would identify the actual design concern.
For example:
This service currently owns validation, payment integration, persistence, and notification behavior. These responsibilities change independently. Could we extract the payment and notification concerns and keep this service focused on coordinating the order use case?
This gives the developer a concrete technical reason and an actionable direction.
Real-Project Question
Question: Why can splitting a God Class into multiple Spring services still produce a bad design?
Answer:
Because class extraction alone does not guarantee good responsibility boundaries.
Poor refactoring can create:
- Circular dependencies
- Excessive delegation
- Generic helper classes
- Tiny meaningless services
- Distributed business logic
- Unclear transaction ownership
- Difficult navigation
The goal is high cohesion and clear ownership, not simply more classes.
29. Quick Rule to Remember
If a class changes for several unrelated business or technical reasons, review whether those responsibilities need separate owners.
30. Final Takeaway
Avoiding God Classes is not about enforcing a maximum number of methods or lines.
The real objective is to keep Java code understandable, maintainable, testable, and safe to change.
What the Developer Should Remember
A service should have a clear responsibility.
When validation, persistence, pricing, external APIs, notification, auditing, and other unrelated concerns accumulate in one class, identify meaningful boundaries and extract behavior where it provides real value.
What the Reviewer Should Check
During Pull Request review, examine:
- Responsibility boundaries
- Cohesion
- Dependency count
- Reasons to change
- Transaction boundaries
- Database access patterns
- External integrations
- Failure handling
- Testability
- Team-maintenance impact
Do not reject code merely because a class is large.
Explain which responsibilities are mixed and why that creates a real maintenance or production risk.
What Should Be Avoided in Production Code
Avoid turning central service classes into dumping grounds for every new requirement.
At the same time, avoid over-engineering the solution into dozens of meaningless classes.
A production-quality design keeps responsibilities focused, dependencies intentional, transaction boundaries clear, and business workflows easy to understand.