1. Introduction
High Cohesion means that the responsibilities inside a class, method, module, or component are closely related to each other and serve one clear purpose.
In practical Java development, highly cohesive code is easier to understand because a developer can look at a class and quickly answer:
What is this class responsible for?
A class with high cohesion usually contains behavior that belongs to the same business capability.
For example, an OrderPricingService may be responsible for:
- Calculating item totals
- Applying discounts
- Calculating taxes
- Calculating final payable amount
These operations belong to one cohesive responsibility: order pricing.
A poorly cohesive class may instead handle:
- Order pricing
- Sending emails
- Uploading files
- Generating PDF invoices
- Updating customer passwords
- Calling external APIs
Although such a class may compile and work, it becomes difficult to maintain, test, review, and extend.
High cohesion is therefore an important practical design quality in Java code reviews.
It often works together with:
- Single Responsibility Principle
- Separation of Concerns
- Low Coupling
- Dependency Inversion
- Interface Segregation
- Clean Architecture
2. What This Topic Means
High cohesion means that the code grouped together inside a component belongs together logically.
Consider:
@Service
public class OrderPricingService {
public BigDecimal calculateSubtotal(Order order) {
return order.getItems()
.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal calculateDiscount(Order order) {
return BigDecimal.ZERO;
}
public BigDecimal calculateFinalPrice(Order order) {
return calculateSubtotal(order)
.subtract(calculateDiscount(order));
}
}All methods are related to pricing.
This class has reasonably high cohesion.
Now consider:
@Service
public class OrderService {
public BigDecimal calculatePrice(Order order) {
return BigDecimal.ZERO;
}
public void sendEmail(Order order) {
}
public byte[] generateInvoice(Order order) {
return new byte[0];
}
public void updateCustomerAddress(Customer customer) {
}
public void uploadInvoiceToS3(byte[] invoice) {
}
}The methods deal with several unrelated responsibilities.
This class has low cohesion.
During code review, the key question is:
Do the fields and methods in this class work together toward one clear responsibility?
High cohesion is not about making every class tiny.
It is about ensuring that responsibilities inside the class belong together.
3. Why It Matters in Real Projects
Readability
A cohesive class is easier to understand.
If a developer opens:
PaymentValidationServicethey should expect payment-validation logic.
They should not unexpectedly find:
- Email delivery
- Database migration
- PDF generation
- Employee management
Maintainability
Changes stay localized.
If tax-calculation rules change, developers should ideally modify pricing-related classes rather than a giant shared service handling many unrelated workflows.
Debugging
When responsibilities are well separated, developers can narrow production problems faster.
For example:
Order pricing issue -> OrderPricingServiceinstead of searching through a 2,000-line OrderService.
Testability
Highly cohesive classes usually require fewer dependencies and smaller test setups.
A pricing service may require:
- TaxPolicy
- DiscountPolicy
A low-cohesion class may require:
- Repository
- Email client
- Payment API
- S3 client
- Notification service
- PDF generator
- Cache
- Kafka publisher
Tests become harder as unrelated dependencies accumulate.
Reliability
Focused components reduce the risk that a change for one feature unintentionally breaks an unrelated feature.
Team Development
Multiple developers can work on separate cohesive components with fewer merge conflicts.
Scalability of Codebase
As the project grows, cohesive modules help prevent "god classes" and oversized service layers.
4. Core Concept
High cohesion can be understood through one practical rule:
Elements that change for the same reason should generally stay together, while elements that change for unrelated reasons should generally be separated.
Consider an e-commerce checkout module.
These methods are closely related:
calculateSubtotal()
calculateDiscount()
calculateTax()
calculateFinalAmount()They may reasonably belong to:
CheckoutPricingServiceNow consider:
calculateTax()
sendConfirmationEmail()
storeInvoiceOnS3()
resetCustomerPassword()These operations change for completely different reasons.
For example:
- Tax rules change because of tax regulations.
- Email changes because of communication requirements.
- S3 changes because of infrastructure requirements.
- Password rules change because of security requirements.
Keeping them together creates low cohesion.
Cohesion at Different Levels
High cohesion can apply to:
- Methods
- Classes
- Packages
- Modules
- Microservices
A method should perform one coherent operation.
A class should represent a focused responsibility.
A package should group related domain functionality.
A microservice should represent a meaningful bounded business capability rather than unrelated endpoints collected randomly.
5. Important Rules
When writing or reviewing Java code:
- Keep closely related behavior together.
- Separate responsibilities that change for unrelated reasons.
- Avoid god classes containing unrelated business and infrastructure logic.
- Avoid utility classes that become dumping grounds for random methods.
- Keep service classes focused on one business capability.
- Keep repository classes focused on persistence.
- Keep controllers focused on HTTP concerns.
- Keep mappers focused on data transformation.
- Keep validators focused on validation.
- Keep external-integration logic out of unrelated business classes.
- Review whether class fields are actually used by most methods.
- Question classes with many unrelated dependencies.
- Prefer meaningful domain-specific components over generic
CommonServiceclasses. - Avoid splitting a cohesive workflow into excessive tiny classes.
- Keep methods focused enough that their name accurately describes the complete behavior.
- Group code based on business responsibility rather than convenience.
- Use package structure to reinforce cohesion.
- Refactor gradually when existing large classes contain several responsibilities.
6. Bad Code Example
Consider an e-commerce application with the following Spring Boot service.
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EmailSender emailSender;
private final InvoiceGenerator invoiceGenerator;
private final CloudStorage cloudStorage;
public CustomerService(
CustomerRepository customerRepository,
OrderRepository orderRepository,
PaymentGateway paymentGateway,
EmailSender emailSender,
InvoiceGenerator invoiceGenerator,
CloudStorage cloudStorage
) {
this.customerRepository = customerRepository;
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.emailSender = emailSender;
this.invoiceGenerator = invoiceGenerator;
this.cloudStorage = cloudStorage;
}
public Customer updateCustomerProfile(Customer customer) {
return customerRepository.save(customer);
}
public Order placeOrder(Order order) {
PaymentResult paymentResult = paymentGateway.charge(order.getTotalAmount());
if (!paymentResult.successful()) {
throw new PaymentFailedException();
}
Order savedOrder = orderRepository.save(order);
emailSender.send(
order.getCustomerEmail(),
"Order placed successfully"
);
return savedOrder;
}
public byte[] generateInvoice(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return invoiceGenerator.generate(order);
}
public void uploadInvoice(Long orderId) {
byte[] invoice = generateInvoice(orderId);
cloudStorage.upload(
"invoice-" + orderId + ".pdf",
invoice
);
}
public void sendMarketingEmail(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
emailSender.send(
customer.getEmail(),
"Check our latest offers"
);
}
}The class is named:
CustomerServicebut contains:
- Customer-profile management
- Order processing
- Payment processing
- Invoice generation
- Cloud storage
- Marketing communication
This is a clear cohesion problem.
7. Problems in the Bad Code
Unrelated Responsibilities
The class handles several unrelated business areas.
Misleading Class Name
CustomerService does not describe most of the class behavior.
Too Many Dependencies
The constructor requires:
- CustomerRepository
- OrderRepository
- PaymentGateway
- EmailSender
- InvoiceGenerator
- CloudStorage
A growing dependency list often indicates that the class is coordinating too many unrelated responsibilities.
Difficult Testing
Testing updateCustomerProfile() requires constructing a class that also depends on payment, invoice, and cloud-storage components.
Change Amplification
Changes in:
- Order workflow
- Invoice generation
- Marketing
- Storage
all modify the same class.
Merge Conflict Risk
Different teams may frequently change the same large service.
Debugging Difficulty
The class becomes a central location for unrelated failures.
Violates Clear Service Boundaries
Customer-profile operations should not normally own payment or invoice infrastructure.
8. Code Review Findings
A senior reviewer should notice:
Finding 1
CustomerService contains business logic from several unrelated domains.
Finding 2
The class name does not match its actual responsibilities.
Finding 3
The six constructor dependencies suggest a possible god-service smell.
The dependency count alone is not proof, but combined with unrelated methods it is strong evidence.
Finding 4
placeOrder() should likely belong to an order-processing component.
Finding 5
Invoice generation and cloud-storage concerns should not be part of customer-profile management.
Finding 6
Marketing-email functionality is another independent responsibility.
Finding 7
Future changes to unrelated features will continue increasing this class size and coupling.
9. Reviewer Comment Example
A practical PR comment could be:
CustomerServiceis currently handling profile management, order placement, invoice processing, storage, and marketing communication. These responsibilities change independently. Could we extract them into cohesive services such as CustomerProfileService, OrderService, InvoiceService, and CustomerNotificationService?
Another:
The constructor now has dependencies for customer data, orders, payment, email, invoice generation, and cloud storage. That looks like a sign that this service has accumulated unrelated responsibilities. Please consider splitting by business capability.
Another:
uploadInvoice()does not appear to belong to customer-profile management. Moving invoice generation/storage behind an InvoiceService would keep CustomerService focused and easier to test.
10. Improved Code
Separate the responsibilities according to business capability.
Customer profile management:
@Service
public class CustomerProfileService {
private final CustomerRepository customerRepository;
public CustomerProfileService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Customer updateProfile(Customer customer) {
return customerRepository.save(customer);
}
}Order processing:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final OrderNotificationSender notificationSender;
public OrderService(
OrderRepository orderRepository,
PaymentGateway paymentGateway,
OrderNotificationSender notificationSender
) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.notificationSender = notificationSender;
}
public Order placeOrder(Order order) {
PaymentResult paymentResult = paymentGateway.charge(order.getTotalAmount());
if (!paymentResult.successful()) {
throw new PaymentFailedException();
}
Order savedOrder = orderRepository.save(order);
notificationSender.sendOrderConfirmation(savedOrder);
return savedOrder;
}
}Invoice processing:
@Service
public class InvoiceService {
private final OrderRepository orderRepository;
private final InvoiceGenerator invoiceGenerator;
private final CloudStorage cloudStorage;
public InvoiceService(
OrderRepository orderRepository,
InvoiceGenerator invoiceGenerator,
CloudStorage cloudStorage
) {
this.orderRepository = orderRepository;
this.invoiceGenerator = invoiceGenerator;
this.cloudStorage = cloudStorage;
}
public byte[] generateInvoice(Long orderId) {
Order order = getOrder(orderId);
return invoiceGenerator.generate(order);
}
public void generateAndStoreInvoice(Long orderId) {
byte[] invoice = generateInvoice(orderId);
cloudStorage.upload(
"invoice-" + orderId + ".pdf",
invoice
);
}
private Order getOrder(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
}Marketing communication:
@Service
public class CustomerMarketingService {
private final CustomerRepository customerRepository;
private final MarketingEmailSender marketingEmailSender;
public CustomerMarketingService(
CustomerRepository customerRepository,
MarketingEmailSender marketingEmailSender
) {
this.customerRepository = customerRepository;
this.marketingEmailSender = marketingEmailSender;
}
public void sendLatestOffers(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
marketingEmailSender.sendLatestOffers(customer);
}
}11. Improved Code Explanation
CustomerProfileService
Contains only customer-profile behavior.
Its responsibility is immediately clear.
OrderService
Handles the order-placement workflow:
- Process payment
- Save order
- Trigger order confirmation
These steps belong to one cohesive business use case.
InvoiceService
Handles invoice-related operations:
- Load order
- Generate invoice
- Store invoice
The methods work together toward one responsibility.
CustomerMarketingService
Owns marketing communication.
Marketing requirements can change independently from customer-profile management.
Smaller Dependency Sets
Each class now receives only dependencies needed for its responsibility.
For example:
CustomerProfileServiceneeds only:
CustomerRepositoryThis makes the design easier to understand and test.
Changes Become Localized
An invoice storage change affects:
InvoiceServicerather than a large generic customer service.
12. Bad Code vs Improved Code
| Area | Bad Code | Improved Code |
|---|---|---|
| Responsibility | Multiple unrelated responsibilities | Each class has a focused responsibility |
| Class naming | Misleading | Names reflect behavior |
| Dependencies | Many unrelated dependencies | Dependencies are capability-specific |
| Readability | Difficult to understand class purpose | Purpose is immediately visible |
| Maintainability | Unrelated changes affect same class | Changes are localized |
| Testability | Large test setup | Smaller focused tests |
| Team development | High merge-conflict risk | Work distributed across components |
| Reliability | Changes have wider blast radius | Smaller change scope |
13. Real Project Scenario
Consider a healthcare application containing:
PatientServiceOver several years, developers add:
- Patient registration
- Patient search
- Insurance eligibility verification
- Appointment scheduling
- Prescription generation
- Lab-order creation
- Report upload
- SMS reminders
- Billing calculation
- Audit logging
The class eventually becomes:
PatientServicewith more than 2,000 lines.
Different teams modify it:
- Registration team
- Billing team
- Appointment team
- Pharmacy team
- Laboratory team
Every release produces merge conflicts.
Testing one small change requires preparing mocks for many unrelated dependencies.
Production incidents become harder to isolate because the same class participates in most workflows.
A better design groups responsibilities into cohesive components such as:
PatientRegistrationService
PatientQueryService
InsuranceEligibilityService
AppointmentService
PrescriptionService
LaboratoryOrderService
PatientDocumentService
PatientNotificationService
BillingServiceThese names communicate responsibilities clearly and allow teams to work independently.
14. Production Impact
Low cohesion does not automatically cause immediate runtime failure.
Its biggest impact usually appears as the system grows.
Increased Regression Risk
Changes for one feature may accidentally affect unrelated behavior.
Difficult Debugging
Large services make it harder to locate the source of failures.
Slower Development
Developers spend more time understanding large classes before making changes.
Merge Conflicts
Multiple teams modify the same central class.
Large Test Suites
Tests may require many mocks and extensive setup.
Deployment Risk
A seemingly small change to one responsibility may require redeploying and retesting a component containing many unrelated workflows.
Maintenance Problems
Low-cohesion classes tend to grow continuously because developers keep adding "one more method" to an existing service.
15. Common Developer Mistakes
Adding Methods Based on Entity Name
A developer sees:
CustomerServiceand adds every customer-related operation there.
But "related to customer" may still represent many different responsibilities.
Creating Generic CommonService Classes
Examples:
CommonService
UtilityService
GeneralService
AppServiceThese classes often become dumping grounds.
Measuring Cohesion Only by Class Size
A 300-line class can be cohesive.
A 50-line class can contain unrelated responsibilities.
Line count is only a signal, not the definition.
Splitting Classes Too Aggressively
High cohesion does not require every method to live in its own class.
Related operations should stay together.
Mixing Business and Infrastructure Concerns
Example:
calculateInvoice()
openS3Connection()
sendSmtpEmail()in the same business class.
Adding Unrelated Helper Methods
Private methods can also reduce cohesion when they implement unrelated functionality.
Reusing a Service Only Because It Already Has a Dependency
A developer may add email logic to a class simply because it already injects an email sender.
Convenience is not a good responsibility boundary.
16. Edge Cases
Large Cohesive Class
A large class is not automatically low cohesion.
For example, a complex tax-calculation component may contain many methods that all support tax calculation.
Reviewers should examine responsibility relationships, not only line count.
Small Low-Cohesion Class
A small class containing:
validatePassword()
generateInvoice()
sendSms()still has poor cohesion even if it contains only 30 lines.
Orchestration Services
Some application services legitimately coordinate several dependencies.
For example:
CheckoutServicemay coordinate:
- Inventory
- Payment
- Order persistence
- Notification
This can still be cohesive if all interactions belong to the single checkout use case.
The key question is whether the dependencies support one workflow or unrelated workflows.
Shared Utility Code
Utility methods may be cohesive if they belong to one technical capability.
For example:
DateTimeFormatterUtilsmay be reasonable.
A generic:
AppUtilscontaining date formatting, encryption, JSON parsing, and file uploads is not cohesive.
Domain Aggregates
Some domain objects legitimately contain several related behaviors because those behaviors operate on the same business invariant.
Do not split domain behavior mechanically.
17. Performance Considerations
High cohesion is primarily a maintainability and design concern.
It does not automatically improve algorithmic performance.
However, low-cohesion designs can create indirect performance problems.
Unnecessary Dependency Initialization
Large classes may require expensive dependencies unrelated to some operations.
Hidden Database Calls
A god service may accumulate repository calls throughout many methods, making data-access patterns difficult to reason about.
Duplicate Processing
Poor separation can lead to duplicated logic across unrelated workflows.
Large Transaction Boundaries
If a low-cohesion service wraps unrelated work inside broad transactions, database locks may be held longer than necessary.
External Calls
Mixing unrelated remote calls into one service can make latency difficult to understand and optimize.
Still, reviewers should not claim:
High cohesion automatically makes code faster.
Its primary benefits are:
- Maintainability
- Readability
- Testability
- Change isolation
18. Security Considerations
Cohesion itself is not a security mechanism, but low-cohesion classes can make security controls harder to understand.
Consider a class containing:
- User lookup
- Password reset
- Role assignment
- Reporting
A reporting operation may unexpectedly share a dependency capable of administrative actions.
Highly cohesive services can create clearer security boundaries.
For example:
UserReader
PasswordResetService
RoleAdministrationServiceThis makes authorization requirements easier to identify.
Sensitive Logging
A large generic service may mix normal operational logging with sensitive workflows.
Separating security-sensitive responsibilities helps reviewers inspect them independently.
Authorization
A cohesive administrative service can enforce authorization consistently.
Secrets
Infrastructure-related credentials should not be spread throughout business services.
High cohesion supports clearer locations for security-sensitive integration code.
However, proper:
- Authentication
- Authorization
- Input validation
- Secret management
must still be implemented explicitly.
19. Testing Considerations
Highly cohesive classes are usually easier to test because each class has a smaller behavioral scope.
Unit Tests
For:
CustomerProfileServicetest:
- Successful profile update
- Invalid customer data
- Repository failure
- Missing customer if applicable
Do not require payment or invoice mocks.
OrderService Tests
Test:
- Payment success
- Payment failure
- Order persistence
- Notification interaction
- Repository exception
InvoiceService Tests
Test:
- Existing order
- Missing order
- Invoice generation failure
- Storage failure
Negative Tests
Verify business-specific failures independently.
Integration Tests
Integration tests can focus on the capability boundary.
For example:
InvoiceService -> InvoiceGenerator -> CloudStorageTest Smell
A unit test requiring 10 or 15 mocks is a strong signal that the production class may have too many responsibilities.
It is not absolute proof, but reviewers should investigate.
20. Refactoring Guidelines
Low-cohesion production classes should be refactored carefully.
Step 1: Identify Responsibilities
List the class methods and group them by business purpose.
Example:
updateCustomer()
changeCustomerAddress()Group:
Customer profileMethods:
createOrder()
cancelOrder()Group:
Order managementMethods:
generateInvoice()
uploadInvoice()Group:
Invoice processingStep 2: Identify Dependencies Per Group
Determine which fields each group actually uses.
This often reveals natural class boundaries.
Step 3: Add Tests Before Moving Logic
Create characterization tests for important existing behavior.
Step 4: Extract One Responsibility at a Time
Avoid rewriting the entire class in one large PR.
For example:
First extract:
InvoiceServiceThen:
MarketingServiceThen:
OrderServiceStep 5: Delegate Temporarily
If many callers depend on the old service, keep a temporary delegate while migrating consumers.
Step 6: Update Consumers
Inject the new focused component directly.
Step 7: Remove Dead Dependencies
After extraction, remove fields no longer required by the original class.
Step 8: Rename Classes
Ensure names accurately reflect remaining responsibilities.
21. Best Practices
- Give each class a clear purpose.
- Keep related business rules together.
- Separate responsibilities that change independently.
- Use domain-specific names.
- Keep controllers thin and HTTP-focused.
- Keep repositories persistence-focused.
- Keep integration adapters provider-focused.
- Keep validators validation-focused.
- Keep mappers transformation-focused.
- Use application services for cohesive workflows.
- Keep dependency lists meaningful.
- Review classes that continuously accumulate methods.
- Group methods by actual business capability.
- Use packages or modules to reinforce cohesive boundaries.
- Refactor incrementally rather than performing risky rewrites.
- Keep orchestration together when it represents one coherent use case.
- Avoid creating classes merely to satisfy arbitrary size limits.
22. Practices to Avoid
God Classes
Avoid classes that control a large portion of the application.
CommonService Dumping Grounds
Avoid:
CommonServiceas a location for unrelated business operations.
Generic Utility Classes
Avoid:
ApplicationUtilscontaining unrelated helpers.
Mixing Layers
Avoid service classes containing:
- SQL construction
- HTTP parsing
- Business rules
- Email sending
- File storage
without a clear reason.
Splitting Cohesive Workflows Unnecessarily
Do not turn every three-line method into a separate Spring bean.
This creates navigation overhead and excessive abstraction.
Grouping Only by Entity
Do not assume every operation involving Customer belongs to one service.
Customer:
- Billing
- Authentication
- Reporting
- Marketing
- Profile management
may represent separate responsibilities.
Dependency Hoarding
Do not keep adding constructor dependencies without reviewing whether the class still represents one cohesive responsibility.
23. Code Review Checklist
Ask these questions during Pull Request review:
- Can I describe this class responsibility in one clear sentence?
- Are most methods related to the same business capability?
- Do the class fields support the same responsibility?
- Are there methods that use completely different dependency groups?
- Does the class name accurately represent all important behavior?
- Is this class becoming a god service?
- Are unrelated methods being added simply because the class already exists?
- Does the class mix business logic with infrastructure details?
- Does the constructor contain many unrelated dependencies?
- Are customer, order, payment, notification, and reporting responsibilities mixed together?
- Could one part of the class change for a completely different reason from another part?
- Would extracting a responsibility simplify testing?
- Does a unit test require many unrelated mocks?
- Are several teams frequently modifying the same class?
- Is a
CommonServiceorUtilsclass becoming a dumping ground? - Are methods grouped by real domain capability?
- Would splitting this class reduce coupling without creating unnecessary complexity?
- Is the proposed extraction meaningful or just mechanical?
- Does an orchestration service still represent one coherent use case?
- Are package and class boundaries aligned with business responsibilities?
24. Common Pull Request Review Comments
- *This service now handles profile updates, billing, notifications, and reporting. These responsibilities change independently; can we extract them into cohesive services?*
- *The new method does not appear related to the existing purpose of this class. Could it belong to InvoiceService instead?*
- *The constructor has grown to nine dependencies across several unrelated domains. That looks like a sign that this service is taking on too many responsibilities.*
- *This class is named CustomerService, but most of the new code is order-processing logic. Please consider moving the order workflow behind a dedicated service.*
- *The unit test needs mocks for payment, storage, email, repository, and reporting even though this method only updates a profile. Splitting responsibilities would make the dependency boundary much clearer.*
- *Please avoid adding this helper to CommonUtils. It is payment-specific behavior and would be more cohesive near the payment module.*
- *Invoice generation and S3 upload form a separate capability from customer management. Could we move them into an InvoiceService?*
- *This extraction may be too granular. These methods all participate in the same pricing calculation and are cohesive enough to remain together.*
- *The class has methods used by three separate business workflows. Can we group them based on their actual reasons to change?*
- *Before adding another dependency here, please check whether the new behavior belongs to the responsibility this service currently owns.*
25. Code Review Exercise
Review the following Spring Boot service.
@Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
private final PayrollRepository payrollRepository;
private final EmailSender emailSender;
private final PdfGenerator pdfGenerator;
private final CloudStorage cloudStorage;
private final AuditPublisher auditPublisher;
public EmployeeService(
EmployeeRepository employeeRepository,
PayrollRepository payrollRepository,
EmailSender emailSender,
PdfGenerator pdfGenerator,
CloudStorage cloudStorage,
AuditPublisher auditPublisher
) {
this.employeeRepository = employeeRepository;
this.payrollRepository = payrollRepository;
this.emailSender = emailSender;
this.pdfGenerator = pdfGenerator;
this.cloudStorage = cloudStorage;
this.auditPublisher = auditPublisher;
}
public Employee updateEmployee(Employee employee) {
Employee savedEmployee = employeeRepository.save(employee);
auditPublisher.publish("EMPLOYEE_UPDATED", savedEmployee.getId());
return savedEmployee;
}
public BigDecimal calculateSalary(Long employeeId) {
Payroll payroll = payrollRepository.findByEmployeeId(employeeId)
.orElseThrow(() -> new PayrollNotFoundException(employeeId));
return payroll.getBasicSalary()
.add(payroll.getAllowance())
.subtract(payroll.getDeductions());
}
public void sendBirthdayEmail(Long employeeId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new EmployeeNotFoundException(employeeId));
emailSender.send(
employee.getEmail(),
"Happy Birthday"
);
}
public void generateAndUploadSalarySlip(Long employeeId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new EmployeeNotFoundException(employeeId));
BigDecimal salary = calculateSalary(employeeId);
byte[] salarySlip = pdfGenerator.generateSalarySlip(
employee,
salary
);
cloudStorage.upload(
"salary-slip-" + employeeId + ".pdf",
salarySlip
);
}
}Identify:
- Cohesion problems
- Unrelated responsibilities
- Misleading class boundaries
- Dependency smells
- Testing problems
- Maintenance risks
- Possible refactoring boundaries
- Responsibilities that should remain together
Do not reveal the answer until you complete your own review.
26. Exercise Solution
The class contains several separate responsibilities.
Responsibility 1: Employee Profile Management
Method:
updateEmployee()belongs to employee-profile management.
The associated audit event may reasonably remain part of that workflow if employee changes must always be audited.
Responsibility 2: Payroll Calculation
Method:
calculateSalary()belongs to payroll.
Salary rules change independently from employee-profile rules.
Responsibility 3: Employee Communication
Method:
sendBirthdayEmail()belongs to employee communication or engagement.
Responsibility 4: Salary-Slip Processing
Method:
generateAndUploadSalarySlip()belongs to payroll-document generation.
It uses:
- Employee data
- Salary calculation
- PDF generation
- Cloud storage
These dependencies support the salary-slip use case.
Improved Design
Employee profile service:
@Service
public class EmployeeProfileService {
private final EmployeeRepository employeeRepository;
private final AuditPublisher auditPublisher;
public EmployeeProfileService(
EmployeeRepository employeeRepository,
AuditPublisher auditPublisher
) {
this.employeeRepository = employeeRepository;
this.auditPublisher = auditPublisher;
}
public Employee updateEmployee(Employee employee) {
Employee savedEmployee = employeeRepository.save(employee);
auditPublisher.publish(
"EMPLOYEE_UPDATED",
savedEmployee.getId()
);
return savedEmployee;
}
}Payroll service:
@Service
public class PayrollService {
private final PayrollRepository payrollRepository;
public PayrollService(PayrollRepository payrollRepository) {
this.payrollRepository = payrollRepository;
}
public BigDecimal calculateSalary(Long employeeId) {
Payroll payroll = payrollRepository.findByEmployeeId(employeeId)
.orElseThrow(() -> new PayrollNotFoundException(employeeId));
return payroll.getBasicSalary()
.add(payroll.getAllowance())
.subtract(payroll.getDeductions());
}
}Employee communication service:
@Service
public class EmployeeNotificationService {
private final EmployeeRepository employeeRepository;
private final EmailSender emailSender;
public EmployeeNotificationService(
EmployeeRepository employeeRepository,
EmailSender emailSender
) {
this.employeeRepository = employeeRepository;
this.emailSender = emailSender;
}
public void sendBirthdayEmail(Long employeeId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new EmployeeNotFoundException(employeeId));
emailSender.send(
employee.getEmail(),
"Happy Birthday"
);
}
}Salary-slip service:
@Service
public class SalarySlipService {
private final EmployeeRepository employeeRepository;
private final PayrollService payrollService;
private final PdfGenerator pdfGenerator;
private final CloudStorage cloudStorage;
public SalarySlipService(
EmployeeRepository employeeRepository,
PayrollService payrollService,
PdfGenerator pdfGenerator,
CloudStorage cloudStorage
) {
this.employeeRepository = employeeRepository;
this.payrollService = payrollService;
this.pdfGenerator = pdfGenerator;
this.cloudStorage = cloudStorage;
}
public void generateAndUpload(Long employeeId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new EmployeeNotFoundException(employeeId));
BigDecimal salary = payrollService.calculateSalary(employeeId);
byte[] salarySlip = pdfGenerator.generateSalarySlip(
employee,
salary
);
cloudStorage.upload(
"salary-slip-" + employeeId + ".pdf",
salarySlip
);
}
}Why These Changes Help
EmployeeProfileService focuses on profile changes.
PayrollService focuses on salary calculations.
EmployeeNotificationService focuses on employee communication.
SalarySlipService focuses on one cohesive document-generation workflow.
Notice that SalarySlipService still contains multiple steps:
- Load employee
- Calculate salary
- Generate PDF
- Upload document
These should not automatically be split further because they all support one coherent use case:
Generate and store an employee salary slip.
This demonstrates an important point:
High cohesion does not mean one method or one dependency per class.
It means that the dependencies and methods should work together toward one clear responsibility.
27. Interview Perspective
High cohesion often appears in Java interviews through code-review scenarios.
An interviewer may show:
UserServicewith 30 methods related to:
- Authentication
- User profile
- Password reset
- Reports
- Role management
and ask:
What problems do you see?
A strong answer should discuss:
- Low cohesion
- Multiple reasons to change
- God-service smell
- Difficult testing
- Large dependency set
- Poor responsibility boundaries
Another question may be:
Does a large class automatically have low cohesion?
The correct answer is no.
Class size is only a signal.
A large class can still be cohesive if all its methods support one complex responsibility.
Senior interviews may ask:
Can a service with five dependencies still have high cohesion?
Yes.
For example, a checkout workflow may legitimately coordinate:
- Pricing
- Inventory
- Payment
- Persistence
- Event publishing
if all dependencies participate in one cohesive checkout use case.
The important question is whether the dependencies support one reason to change or several unrelated ones.
28. Interview Questions and Answers
Basic Question
Question: What is high cohesion in Java design?
Answer:
High cohesion means that the responsibilities within a class or module are closely related and work toward one clear purpose.
A highly cohesive class usually has methods and dependencies that belong to the same business capability.
Intermediate Question
Question: How can you identify low cohesion during code review?
Answer:
Common indicators include:
- Unrelated methods in one class
- Misleading class names
- Large numbers of unrelated dependencies
- Different methods using completely different dependency groups
- God services
- Generic
CommonServiceorUtilsclasses - Tests requiring many unrelated mocks
- Frequent changes from different feature teams in the same class
These signals should trigger a responsibility-boundary review.
Advanced Question
Question: What is the relationship between high cohesion and Single Responsibility Principle?
Answer:
They are closely related.
SRP says a component should have one reason to change.
High cohesion means the elements within that component are strongly related to that responsibility.
A well-designed class often satisfies both:
- One meaningful responsibility
- Methods and dependencies closely related to that responsibility
However, cohesion is a broader design quality and can also be discussed at package, module, and service boundaries.
Scenario-Based Question
Question: A CheckoutService depends on PricingService, InventoryService, PaymentGateway, OrderRepository, and OrderEventPublisher. Does that automatically indicate low cohesion?
Answer:
No.
If all five dependencies participate in one coherent checkout workflow, the service can still have high cohesion.
For example:
- Calculate price.
- Reserve inventory.
- Charge payment.
- Save order.
- Publish order event.
All operations serve one use case.
Dependency count alone should not determine cohesion.
The reviewer should examine whether dependencies support one workflow or unrelated responsibilities.
Code-Review Question
Question: A class contains profile updates, password reset, user reporting, and marketing email logic. What would you recommend?
Answer:
I would identify the separate responsibilities and consider extracting components such as:
UserProfileService
PasswordResetService
UserReportService
MarketingCommunicationServiceThe exact split should follow real business boundaries rather than arbitrary method count.
I would refactor incrementally and protect existing behavior with tests.
Real-Project Question
Question: How does high cohesion help a large Spring Boot application?
Answer:
High cohesion creates clear service and module boundaries.
It helps:
- Developers understand where logic belongs.
- Teams work independently.
- Tests remain focused.
- Changes remain localized.
- Production issues are easier to diagnose.
- Modules are easier to extract or evolve later.
For example, separating payment, notification, invoice, and order-processing responsibilities creates clearer ownership than putting everything inside one large OrderService.
29. Quick Rule to Remember
If the methods in a class do not belong to the same clear responsibility, the class probably needs better cohesion.
30. Final Takeaway
High cohesion is about keeping code that belongs together in the same place while separating responsibilities that change independently.
Developers should remember:
- A class should have a clear purpose.
- Related methods should stay together.
- Unrelated responsibilities should be separated.
- Large classes are not automatically bad.
- Small classes are not automatically cohesive.
- Dependency count is a signal, not a rule.
- Cohesion should follow real business boundaries.
- Orchestration of several steps can remain cohesive when all steps serve one use case.
- Generic service and utility classes should be reviewed carefully.
During Pull Request review, reviewers should check:
- Whether the class purpose can be described clearly.
- Whether methods belong to the same business capability.
- Whether dependencies are related.
- Whether unrelated responsibilities are accumulating.
- Whether tests require many unrelated mocks.
- Whether a class has become a common dumping ground.
- Whether extracting a responsibility would reduce maintenance risk.
- Whether a proposed extraction actually improves design or only creates extra classes.
Production code should avoid classes where developers cannot clearly answer:
What is this class actually responsible for?
A strong Java design groups closely related responsibilities together and keeps unrelated concerns separated, producing code that is easier to understand, review, test, debug, and maintain.