High Cohesion

22 min read

Object-Oriented Design and SOLID Review — review Java classes for unrelated responsibilities that should be grouped by real business capability instead.

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:

JAVA
@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:

JAVA
@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:

JAVA
PaymentValidationService

they 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:

JAVA
Order pricing issue -> OrderPricingService

instead 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:

JAVA
calculateSubtotal()
calculateDiscount()
calculateTax()
calculateFinalAmount()

They may reasonably belong to:

JAVA
CheckoutPricingService

Now consider:

JAVA
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 CommonService classes.
  • 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.

JAVA
@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:

JAVA
CustomerService

but 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:

CustomerService is 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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
CustomerProfileService

needs only:

JAVA
CustomerRepository

This makes the design easier to understand and test.

Changes Become Localized

An invoice storage change affects:

JAVA
InvoiceService

rather than a large generic customer service.

12. Bad Code vs Improved Code

AreaBad CodeImproved Code
ResponsibilityMultiple unrelated responsibilitiesEach class has a focused responsibility
Class namingMisleadingNames reflect behavior
DependenciesMany unrelated dependenciesDependencies are capability-specific
ReadabilityDifficult to understand class purposePurpose is immediately visible
MaintainabilityUnrelated changes affect same classChanges are localized
TestabilityLarge test setupSmaller focused tests
Team developmentHigh merge-conflict riskWork distributed across components
ReliabilityChanges have wider blast radiusSmaller change scope

13. Real Project Scenario

Consider a healthcare application containing:

JAVA
PatientService

Over 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:

JAVA
PatientService

with 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:

JAVA
PatientRegistrationService
PatientQueryService
InsuranceEligibilityService
AppointmentService
PrescriptionService
LaboratoryOrderService
PatientDocumentService
PatientNotificationService
BillingService

These 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:

JAVA
CustomerService

and adds every customer-related operation there.

But "related to customer" may still represent many different responsibilities.

Creating Generic CommonService Classes

Examples:

JAVA
CommonService
UtilityService
GeneralService
AppService

These 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:

JAVA
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:

JAVA
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:

JAVA
CheckoutService

may 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:

JAVA
DateTimeFormatterUtils

may be reasonable.

A generic:

JAVA
AppUtils

containing 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:

JAVA
UserReader
PasswordResetService
RoleAdministrationService

This 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:

JAVA
CustomerProfileService

test:

  • 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:

JAVA
InvoiceService -> InvoiceGenerator -> CloudStorage

Test 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:

JAVA
updateCustomer()
changeCustomerAddress()

Group:

JAVA
Customer profile

Methods:

JAVA
createOrder()
cancelOrder()

Group:

JAVA
Order management

Methods:

JAVA
generateInvoice()
uploadInvoice()

Group:

JAVA
Invoice processing

Step 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:

JAVA
InvoiceService

Then:

JAVA
MarketingService

Then:

JAVA
OrderService

Step 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:

JAVA
CommonService

as a location for unrelated business operations.

Generic Utility Classes

Avoid:

JAVA
ApplicationUtils

containing 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 CommonService or Utils class 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

  1. *This service now handles profile updates, billing, notifications, and reporting. These responsibilities change independently; can we extract them into cohesive services?*
  1. *The new method does not appear related to the existing purpose of this class. Could it belong to InvoiceService instead?*
  1. *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.*
  1. *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.*
  1. *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.*
  1. *Please avoid adding this helper to CommonUtils. It is payment-specific behavior and would be more cohesive near the payment module.*
  1. *Invoice generation and S3 upload form a separate capability from customer management. Could we move them into an InvoiceService?*
  1. *This extraction may be too granular. These methods all participate in the same pricing calculation and are cohesive enough to remain together.*
  1. *The class has methods used by three separate business workflows. Can we group them based on their actual reasons to change?*
  1. *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.

JAVA
@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:

JAVA
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:

JAVA
calculateSalary()

belongs to payroll.

Salary rules change independently from employee-profile rules.

Responsibility 3: Employee Communication

Method:

JAVA
sendBirthdayEmail()

belongs to employee communication or engagement.

Responsibility 4: Salary-Slip Processing

Method:

JAVA
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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
@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:

JAVA
UserService

with 30 methods related to:

  • Authentication
  • User profile
  • Password reset
  • Reports
  • Email
  • 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 CommonService or Utils classes
  • 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:

  1. Calculate price.
  2. Reserve inventory.
  3. Charge payment.
  4. Save order.
  5. 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:

JAVA
UserProfileService
PasswordResetService
UserReportService
MarketingCommunicationService

The 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.