Writing Maintainable Code for Other Developers

18 min read

Clean Code and Readability — review Java code so another developer can understand and safely change it without needing the original author's explanation.

1. Introduction

Production Java code is rarely maintained only by the developer who originally wrote it.

During the lifetime of an application, the same code may be reviewed, debugged, extended, migrated, optimized, and fixed by many different developers.

A service written today may be modified months later by someone who:

  • Did not participate in the original implementation
  • Does not know the original requirement
  • Is unfamiliar with the surrounding module
  • Is investigating a production incident under time pressure
  • Needs to add a new business rule without breaking existing behavior

Because of this, maintainable code should communicate its intent clearly without requiring the original developer to explain it.

Writing maintainable code means designing classes, methods, names, control flow, dependencies, and business rules so that another developer can understand:

  • What the code does
  • Why the code exists
  • What assumptions it makes
  • Which parts are safe to change
  • Which operations have side effects
  • How failures are handled
  • Where new behavior should be added

The objective is not to make code excessively abstract or heavily documented.

The objective is to make normal Java code predictable, readable, testable, and safe to modify.

2. What This Topic Means

Writing maintainable code for other developers means treating readability and future change as part of the implementation requirement.

A developer should not write code only for the compiler.

The code should also be understandable to another engineer reviewing a Pull Request or maintaining the feature later.

Consider this method:

JAVA
public void handle(Order o) {
    if (o != null && o.getS() == 1) {
        repo.save(o);
        client.send(o);
    }
}

The compiler can understand it.

Another developer must guess:

  • What does getS() mean?
  • Why must it equal 1?
  • What does client.send() send?
  • Does saving happen before notification intentionally?
  • What happens if send() fails?
  • Should null orders be ignored or rejected?

A maintainable version makes those decisions visible:

JAVA
public void confirmOrder(Order order) {
    validateOrder(order);

    if (!order.isReadyForConfirmation()) {
        throw new InvalidOrderStateException(order.getId());
    }

    order.markConfirmed();
    orderRepository.save(order);
    orderNotificationService.sendConfirmation(order);
}

The second version is easier to understand because its structure communicates business intent.

3. Why It Matters in Real Projects

Readability

Developers spend significant time reading existing code before changing it.

Readable code reduces the time required to understand:

  • Business rules
  • Dependencies
  • Failure paths
  • Data flow
  • Side effects

Maintainability

Requirements continually evolve.

Maintainable code allows developers to modify one area without having to understand an entire module.

Debugging

Production debugging becomes easier when:

  • Methods have meaningful names
  • Important states are explicit
  • Exceptions communicate the failure
  • Side effects are separated
  • Logging provides useful context

Reliability

Code that is easy to understand is less likely to be modified incorrectly.

Poor maintainability increases regression risk because developers may not understand hidden assumptions.

Team Development

Software development is collaborative.

Maintainable code helps:

  • Pull Request reviewers
  • New team members
  • Developers working across modules
  • On-call engineers
  • QA engineers
  • Architects

Scalability of Development

Maintainability also affects how efficiently a team can grow.

If only one developer understands a module, that module becomes a knowledge bottleneck.

Clean, predictable code distributes knowledge across the team.

4. Core Concept

The main principle is:

Write code so that another competent Java developer can understand and safely modify it without depending on the original author.

Maintainability comes from several characteristics working together.

Clear Intent

Names should explain the business purpose.

Prefer:

JAVA
calculateRefundAmount()

over:

JAVA
calc()

Focused Responsibilities

Methods and classes should have understandable responsibilities.

Explicit Business Rules

Important business decisions should be visible.

Prefer:

JAVA
if (customer.isBlocked()) {
    throw new CustomerBlockedException(customer.getId());
}

over hiding the same rule inside a complex expression.

Predictable Side Effects

A method called:

JAVA
validatePayment()

should not secretly save an entity or send an email.

Clear Failure Behavior

Exceptions should communicate the actual problem.

Testable Design

Important business behavior should be testable without excessive setup.

Limited Coupling

A change in one component should not unnecessarily require changes in several unrelated components.

5. Important Rules

  • Use meaningful domain-specific names.
  • Keep methods focused on a clear responsibility.
  • Avoid hidden side effects.
  • Keep business rules explicit.
  • Avoid magic numbers and unexplained strings.
  • Use constants or enums for important domain values.
  • Prefer straightforward control flow.
  • Keep database operations visible.
  • Keep external API calls identifiable.
  • Handle exceptional cases deliberately.
  • Do not silently ignore invalid states unless the requirement explicitly allows it.
  • Avoid excessive method parameters.
  • Avoid shared mutable state where unnecessary.
  • Keep dependencies explicit through constructor injection.
  • Do not expose internal implementation details unnecessarily.
  • Keep important transformations easy to trace.
  • Avoid premature abstraction.
  • Avoid copy-paste business logic.
  • Keep comments focused on why, not obvious what.
  • Preserve consistent patterns across similar modules.
  • Design for future modification, not hypothetical future requirements.
  • Make logging useful for operational debugging.
  • Write tests around business behavior.
  • Keep Pull Requests small enough to review where practical.

6. Bad Code Example

The following Spring Boot service handles employee account activation.

JAVA
@Service
public class EmployeeService {
    @Autowired
    EmployeeRepository repo;

    @Autowired
    EmailClient client;

    public String doIt(Long id, boolean x) {
        Employee e = repo.findById(id).orElse(null);

        if (e == null) {
            return "ERR";
        }

        if (x) {
            if (e.getStatus().equals("0")) {
                e.setStatus("1");
                e.setUpdatedAt(LocalDateTime.now());
                repo.save(e);

                try {
                    client.send(
                            e.getEmail(),
                            "Account",
                            "Your account is active"
                    );
                } catch (Exception ex) {
                    System.out.println(ex.getMessage());
                }

                return "OK";
            } else {
                return "INVALID";
            }
        } else {
            e.setStatus("0");
            repo.save(e);
            return "DONE";
        }
    }
}

7. Problems in the Bad Code

Unclear Method Name

doIt() provides no information about the business operation.

A developer searching for employee activation behavior would not know that this method contains it.

Unclear Parameter

The boolean parameter:

JAVA
boolean x

does not reveal whether true means:

  • Activate
  • Enable
  • Verify
  • Notify

Boolean flags often hide multiple behaviors inside one method.

Cryptic Variable Names

Variables such as:

JAVA
e
repo
client

provide weak context.

Magic Status Values

The method uses:

JAVA
"0"
"1"

without explaining what those values represent.

A reviewer must infer that they probably mean inactive and active.

Multiple Responsibilities

The method handles:

  • Employee lookup
  • Activation
  • Deactivation
  • State validation
  • Persistence
  • Email notification
  • Error handling
  • Response status creation

Generic Return Strings

Values such as:

JAVA
"ERR"
"OK"
"INVALID"
"DONE"

do not communicate a stable API contract.

Swallowed Notification Failure

The method catches:

JAVA
Exception

and prints the message.

The failure is effectively hidden from the application.

System.out.println() in Server Code

Production Spring Boot applications should normally use structured logging rather than direct console printing.

Field Injection

Field injection hides required dependencies and makes unit testing more difficult.

Constructor injection makes dependencies explicit.

Null Handling Is Ambiguous

Missing employees return "ERR" instead of a meaningful exception or business result.

Maintenance Risk

A developer changing activation behavior must also understand deactivation, notification, persistence, and return-code rules.

8. Code Review Findings

A senior reviewer should notice:

  • doIt() does not communicate its domain purpose.
  • The boolean parameter selects unrelated behaviors.
  • Magic status values should be replaced with domain-specific representation.
  • Activation and deactivation should be explicit operations.
  • Field injection should be replaced with constructor injection.
  • Returning arbitrary strings creates an unclear service contract.
  • Notification failure is swallowed.
  • System.out.println() is inappropriate for production diagnostics.
  • Exception is too broad.
  • The service mixes account state management and notification failure handling.
  • Business states should be represented consistently.
  • The current method will become harder to maintain as employee lifecycle rules grow.

9. Reviewer Comment Example

A useful PR comment could be:

doIt() currently handles both activation and deactivation based on a boolean flag. Could we expose explicit methods such as activateEmployee() and deactivateEmployee() so the service API communicates the business operation directly?

Another comment:

The "0" and "1" status values are difficult to interpret during review. Please consider using an enum such as EmployeeStatus.ACTIVE/INACTIVE so invalid states become harder to introduce.

Another:

The notification exception is currently swallowed with System.out.println(). Please use the application logger and define whether notification failure should fail the activation request or be handled asynchronously.

10. Improved Code

JAVA
@Service
public class EmployeeAccountService {
    private static final Logger log =
            LoggerFactory.getLogger(EmployeeAccountService.class);

    private final EmployeeRepository employeeRepository;
    private final EmployeeNotificationService notificationService;

    public EmployeeAccountService(EmployeeRepository employeeRepository,
                                  EmployeeNotificationService notificationService) {
        this.employeeRepository = employeeRepository;
        this.notificationService = notificationService;
    }

    @Transactional
    public EmployeeAccountResult activateEmployee(Long employeeId) {
        Employee employee = findEmployee(employeeId);

        if (employee.getStatus() == EmployeeStatus.ACTIVE) {
            throw new EmployeeAlreadyActiveException(employeeId);
        }

        employee.setStatus(EmployeeStatus.ACTIVE);
        employee.setUpdatedAt(LocalDateTime.now());

        Employee savedEmployee = employeeRepository.save(employee);

        sendActivationNotification(savedEmployee);

        return new EmployeeAccountResult(
                savedEmployee.getId(),
                savedEmployee.getStatus()
        );
    }

    @Transactional
    public EmployeeAccountResult deactivateEmployee(Long employeeId) {
        Employee employee = findEmployee(employeeId);

        if (employee.getStatus() == EmployeeStatus.INACTIVE) {
            throw new EmployeeAlreadyInactiveException(employeeId);
        }

        employee.setStatus(EmployeeStatus.INACTIVE);
        employee.setUpdatedAt(LocalDateTime.now());

        Employee savedEmployee = employeeRepository.save(employee);

        return new EmployeeAccountResult(
                savedEmployee.getId(),
                savedEmployee.getStatus()
        );
    }

    private Employee findEmployee(Long employeeId) {
        if (employeeId == null) {
            throw new IllegalArgumentException("Employee ID is required");
        }

        return employeeRepository.findById(employeeId)
                .orElseThrow(() -> new EmployeeNotFoundException(employeeId));
    }

    private void sendActivationNotification(Employee employee) {
        try {
            notificationService.sendActivationEmail(employee);
        } catch (NotificationException ex) {
            log.error(
                    "Failed to send activation email for employeeId={}",
                    employee.getId(),
                    ex
            );
        }
    }
}

The status is represented explicitly:

JAVA
public enum EmployeeStatus {
    ACTIVE,
    INACTIVE
}

The response is also explicit:

JAVA
public record EmployeeAccountResult(
        Long employeeId,
        EmployeeStatus status
) {
}

11. Improved Code Explanation

Business Operations Are Explicit

The original method:

JAVA
doIt(id, true)

has been replaced with:

JAVA
activateEmployee(employeeId)

Similarly:

JAVA
doIt(id, false)

becomes:

JAVA
deactivateEmployee(employeeId)

A caller can understand the operation immediately.

Boolean Flag Was Removed

A boolean parameter was controlling two different flows.

Separate methods make each use case independently understandable.

Domain Status Uses an Enum

Instead of:

JAVA
"0"
"1"

the code now uses:

JAVA
EmployeeStatus.ACTIVE
EmployeeStatus.INACTIVE

This improves:

  • Readability
  • Type safety
  • Refactoring safety
  • IDE navigation

Dependencies Are Explicit

Constructor injection shows exactly what the class requires.

It also makes unit testing easier.

Missing Employee Has Clear Behavior

Instead of returning:

JAVA
"ERR"

the method throws:

JAVA
EmployeeNotFoundException

The failure is explicit and can be mapped to an appropriate API response.

Result Type Is Explicit

A structured result is better than unrelated string return codes.

Logging Is Operationally Useful

Notification failures include:

  • Employee ID
  • Exception stack trace
  • Meaningful message

This is more useful during production investigation.

12. Bad Code vs Improved Code

AreaBad CodeImproved Code
API claritydoIt(id, true)activateEmployee(id)
NamingCryptic namesDomain-specific names
Status"0" and "1"EmployeeStatus enum
Error handlingGeneric stringsSpecific exceptions
Dependency managementField injectionConstructor injection
LoggingSystem.out.println()Structured logger
TestabilityHidden dependencies and multiple pathsExplicit dependencies and focused methods
MaintainabilityActivation and deactivation mixedOperations separated
ReliabilityInvalid states easier to introduceDomain states constrained

13. Real Project Scenario

Consider a banking microservice responsible for customer beneficiary management.

Initially, one service method handles:

  • Creating a beneficiary
  • Updating details
  • Activating the beneficiary
  • Deactivating the beneficiary
  • Validating transfer limits
  • Sending OTP
  • Recording audit information
  • Sending notifications

The behavior is controlled by request flags.

For example:

JAVA
processBeneficiary(request, true, false, true);

Months later, a developer needs to modify only beneficiary activation.

Before changing the code, the developer must understand several unrelated paths controlled by booleans.

The change accidentally bypasses an audit operation during deactivation.

A maintainable API would expose clear operations:

JAVA
createBeneficiary()
updateBeneficiary()
activateBeneficiary()
deactivateBeneficiary()

Shared validation can still be reused internally, but each public operation clearly communicates its purpose.

This significantly reduces maintenance risk.

14. Production Impact

Poor maintainability can affect production indirectly but significantly.

Regression Bugs

A developer may unintentionally affect another business path while modifying tightly coupled code.

Longer Incident Resolution

Unclear naming and hidden side effects increase the time required to understand failures.

Incorrect State Changes

Magic values and loosely defined status strings can produce invalid entity states.

Lost Operational Information

Swallowed exceptions or weak logging can make failures difficult to diagnose.

Inconsistent Behavior

Copy-pasted logic may evolve differently across multiple methods.

Higher Change Risk

Developers become reluctant to modify complex modules because the impact is difficult to predict.

Knowledge Bottlenecks

If only the original developer understands the code, team productivity becomes dependent on that individual.

15. Common Developer Mistakes

Writing for the Current Task Only

A developer implements the immediate requirement without considering how the code will be modified later.

Assuming Everybody Knows the Context

Names such as:

JAVA
flag
type
mode
data
value

may make sense while implementing the feature but become unclear later.

Overusing Boolean Parameters

Methods such as:

JAVA
process(order, true, false);

do not communicate behavior.

Excessive Comments Instead of Better Code

Bad:

JAVA
// Check if customer is active
if (customer.getStatus() == 1) {

Better:

JAVA
if (customer.isActive()) {

Creating Generic Utility Methods Too Early

Developers sometimes create generic abstractions before understanding whether behaviors are actually the same.

Copy-Pasting Business Logic

Repeated logic eventually becomes inconsistent when only one copy is updated.

Swallowing Exceptions

Code such as:

JAVA
try {
    externalService.call();
} catch (Exception ex) {
}

makes production failures invisible.

Returning Null for Unexpected Conditions

Returning null forces every caller to guess what null represents.

Magic Values

Avoid unexplained values such as:

JAVA
0
1
999
"A"
"Y"
"N"

when domain types can communicate intent better.

16. Edge Cases

Null Inputs

Public service methods should define whether null input:

  • Is invalid
  • Is optional
  • Represents absence

Do not allow behavior to emerge accidentally from a NullPointerException.

Empty Collections

Define how empty requests should behave.

For example, an empty order may be:

  • Invalid
  • Valid but zero-valued
  • Ignored

Duplicate Data

If duplicate IDs or entries are possible, define whether they should:

  • Be rejected
  • Be merged
  • Be processed independently

Invalid State Transitions

Examples:

  • Activating an already active employee
  • Cancelling an already refunded payment
  • Shipping a cancelled order

State transitions should be explicit.

External Service Failure

Maintainable code should make the expected behavior visible when:

  • Email fails
  • Payment times out
  • External REST API returns 500
  • Message publication fails

Concurrency

If several requests can update the same entity, reviewers should consider:

  • Optimistic locking
  • Transaction boundaries
  • Duplicate processing
  • Idempotency

17. Performance Considerations

Maintainability itself does not require sacrificing performance.

In most business applications, clear Java code and efficient Java code are compatible.

Database Calls

Maintainable code should make database interaction visible.

A method such as:

JAVA
loadOrders()

should not unexpectedly execute dozens of queries.

Reviewers should identify patterns such as:

JAVA
for (Long id : orderIds) {
    orderRepository.findById(id);
}

A bulk query may be more appropriate.

External API Calls

Avoid hiding network calls inside generic helpers or collection operations.

Network I/O is important enough to remain visible.

Repeated Computation

Meaningful local variables can improve both clarity and performance.

Instead of:

JAVA
if (calculateRisk(customer) > 10
        && calculateRisk(customer) < 50) {

use:

JAVA
int riskScore = calculateRisk(customer);

Object Creation

Do not introduce unnecessary wrapper objects purely for abstraction.

Performance Optimization

If optimization makes code substantially more complex, document why it is necessary and support the decision with measurements.

Do not reduce maintainability for hypothetical performance gains.

18. Security Considerations

Maintainability is especially important in security-sensitive code.

Authorization behavior should be easy for reviewers to verify.

Avoid code such as:

JAVA
if (u != null && (u.isAdmin() || u.getId().equals(r.getOwnerId()) && !r.isLocked())) {

Prefer explicit security decisions:

JAVA
if (!canAccessResource(user, resource)) {
    throw new AccessDeniedException("Access denied");
}

Security-related maintainability includes:

  • Clear authorization checks
  • Explicit authentication assumptions
  • Safe input validation
  • Sensitive-data masking
  • Avoiding secrets in source code
  • Safe logging
  • Clear tenant isolation
  • Explicit permission rules

Security code should not depend on clever expressions that are difficult to audit.

19. Testing Considerations

Maintainable code should make business behavior straightforward to test.

Positive Tests

For employee activation:

  • Inactive employee becomes active.
  • Updated timestamp is set.
  • Employee is saved.
  • Activation notification is triggered.

Negative Tests

  • Missing employee ID is rejected.
  • Unknown employee ID throws EmployeeNotFoundException.
  • Already active employee cannot be activated again.

Exception Tests

Verify behavior when:

  • Repository fails
  • Notification service fails
  • Database update fails

Boundary Cases

Where applicable, test:

  • Minimum allowed values
  • Maximum allowed values
  • Empty collections
  • State transition boundaries

Unit Tests

Constructor injection makes collaborators easy to mock.

For example:

JAVA
@ExtendWith(MockitoExtension.class)
class EmployeeAccountServiceTest {
    @Mock
    private EmployeeRepository employeeRepository;

    @Mock
    private EmployeeNotificationService notificationService;

    @InjectMocks
    private EmployeeAccountService employeeAccountService;

    @Test
    void shouldActivateInactiveEmployee() {
        Employee employee = new Employee();
        employee.setId(101L);
        employee.setStatus(EmployeeStatus.INACTIVE);

        when(employeeRepository.findById(101L))
                .thenReturn(Optional.of(employee));

        when(employeeRepository.save(employee))
                .thenReturn(employee);

        EmployeeAccountResult result =
                employeeAccountService.activateEmployee(101L);

        assertEquals(EmployeeStatus.ACTIVE, result.status());

        verify(employeeRepository).save(employee);
        verify(notificationService).sendActivationEmail(employee);
    }
}

Integration Tests

Use integration tests to verify:

  • Persistence mappings
  • Transaction behavior
  • API error responses
  • State changes in the database

20. Refactoring Guidelines

Step 1: Understand Current Behavior

Before changing code, identify:

  • Inputs
  • Outputs
  • Side effects
  • Exceptions
  • Database updates
  • External calls

Step 2: Protect Existing Behavior

Add or verify tests before structural refactoring.

Step 3: Improve Names First

Renaming variables and methods often exposes design problems without changing behavior.

Step 4: Replace Magic Values

Convert important strings or numbers to:

  • Enums
  • Constants
  • Domain types

Step 5: Separate Different Operations

If one method performs multiple business actions controlled by flags, consider explicit methods.

Step 6: Extract Business Rules

Move complex conditions into meaningful operations.

Step 7: Make Dependencies Explicit

Prefer constructor injection.

Step 8: Improve Failure Handling

Replace:

  • Null returns
  • Generic error strings
  • Swallowed exceptions

with deliberate behavior.

Step 9: Review Side Effects

Make database and external service calls easy to identify.

Step 10: Refactor Incrementally

Avoid combining a large cleanup with unrelated feature development where possible.

Smaller refactoring steps are easier to review and verify.

21. Best Practices

  • Write meaningful class names.
  • Write meaningful method names.
  • Use domain terminology consistently.
  • Keep public service APIs explicit.
  • Prefer enums over magic status values.
  • Use constructor injection.
  • Keep methods focused.
  • Keep business rules near the relevant business operation.
  • Use specific exceptions.
  • Use structured logging.
  • Keep side effects visible.
  • Avoid excessive nesting.
  • Use early returns where they simplify logic.
  • Keep important constants named.
  • Avoid unnecessary abstraction.
  • Reuse logic only when behavior is genuinely the same.
  • Keep transaction boundaries understandable.
  • Make external integration behavior explicit.
  • Design result objects around meaningful API contracts.
  • Write tests around behavior rather than implementation details.
  • Keep code consistent with established project conventions.

22. Practices to Avoid

Generic Method Names

Avoid:

JAVA
process()
handle()
execute()
doWork()
manage()

unless the surrounding abstraction gives them very clear meaning.

Boolean Flags Controlling Major Behavior

Avoid:

JAVA
processOrder(order, true, false);

Prefer explicit operations.

Magic Values

Avoid unexplained state codes.

Swallowed Exceptions

Never ignore failures without a deliberate reason.

Comments Explaining Poor Names

Do not use comments as a substitute for readable code.

Excessive Abstraction

A developer should not navigate through ten interfaces and factories to understand a simple calculation.

Copy-Paste Logic

Repeated business rules will eventually diverge.

Hidden Side Effects

A method named:

JAVA
buildInvoice()

should not unexpectedly send an email.

Large Utility Classes

Classes such as:

JAVA
CommonUtil
GeneralHelper
ApplicationUtils

often become dumping grounds for unrelated behavior.

Static Mutable State

Avoid shared mutable state unless the design genuinely requires it.

23. Code Review Checklist

A reviewer can ask:

  • Can another developer understand the purpose of this class quickly?
  • Do method names describe business intent?
  • Are variable names meaningful?
  • Are important domain states represented explicitly?
  • Are there unexplained strings or numeric values?
  • Does one method perform several unrelated responsibilities?
  • Are boolean parameters hiding different business operations?
  • Are side effects visible?
  • Are database calls easy to identify?
  • Are external API calls easy to identify?
  • Are exceptions handled deliberately?
  • Are failures being silently ignored?
  • Is structured logging used appropriately?
  • Do logs contain useful operational context?
  • Are sensitive values excluded from logs?
  • Are dependencies explicit?
  • Is constructor injection used?
  • Are transaction boundaries understandable?
  • Can important business behavior be unit tested?
  • Are there duplicated business rules?
  • Are null and invalid-state behaviors clearly defined?
  • Would another developer know where to add a future business rule?
  • Is the implementation simpler than the problem requires, or unnecessarily abstract?
  • Does this code follow the conventions already used in the project?
  • Can this code be changed safely without asking the original developer how it works?

24. Common Pull Request Review Comments

  1. *Could we rename this method to reflect the actual business operation? processData() does not make its responsibility clear.*
  1. *This boolean flag changes the method from activation to deactivation behavior. Please consider exposing separate methods so callers do not need to understand what true/false means.*
  1. *The status values "0" and "1" are domain concepts. An enum would make the code easier to read and prevent invalid values.*
  1. *This exception is currently swallowed. Please define the expected failure behavior and add structured logging if the operation is intentionally non-blocking.*
  1. *Could we use constructor injection here? These dependencies are required by the service and should be explicit.*
  1. *This helper method both validates the request and updates the database. Please separate the responsibilities or rename it so the side effect is obvious.*
  1. *There are three copies of this eligibility rule in the service. Consider keeping the rule in one place so future changes do not produce inconsistent behavior.*
  1. *Please replace the generic "ERROR" return value with a meaningful exception or typed result so callers can distinguish failure cases.*
  1. *This comment explains what the code is doing because the variable names are unclear. Renaming the variables would make the code self-explanatory.*
  1. *The implementation works, but the abstraction adds several layers for a simple operation. Can we simplify this while preserving the required behavior?*

25. Code Review Exercise

Review the following payment refund service.

Identify:

  • Problems
  • Code smells
  • Risks
  • Improvements
JAVA
@Service
public class RefundService {
    @Autowired
    private PaymentRepository repo;

    @Autowired
    private PaymentGateway gateway;

    public String process(Long id, int type) {
        Payment p = repo.findById(id).orElse(null);

        if (p == null) {
            return null;
        }

        if (type == 1) {
            if ("SUCCESS".equals(p.getStatus())) {
                if (p.getAmount().compareTo(BigDecimal.ZERO) > 0) {
                    try {
                        String ref = gateway.refund(
                                p.getTransactionId(),
                                p.getAmount()
                        );

                        p.setStatus("REFUNDED");
                        p.setRefundReference(ref);
                        repo.save(p);

                        return "OK";
                    } catch (Exception e) {
                        System.out.println("refund failed");
                        return "FAIL";
                    }
                }
            }
        }

        if (type == 2) {
            p.setStatus("CANCELLED");
            repo.save(p);
            return "DONE";
        }

        return "INVALID";
    }
}

26. Exercise Solution

Review Findings

Generic Method Name

process() does not indicate whether the method performs:

  • Refund
  • Cancellation
  • Validation

Magic Operation Types

The caller must know:

JAVA
1 = refund
2 = cancellation

This creates an unclear API contract.

Cryptic Variables

Variables such as:

JAVA
p
repo
ref

provide minimal business context.

Generic String Status Values

Payment states are represented through unrestricted strings.

Null Return

Missing payment returns null, which does not communicate the reason for failure.

Multiple Business Operations

Refund and cancellation are unrelated operations controlled by type.

Excessive Nesting

Refund validation is deeply nested.

Broad Exception Handling

Exception catches failures that may not belong to payment gateway processing.

Weak Logging

System.out.println("refund failed") does not include:

  • Payment ID
  • Transaction ID
  • Exception
  • Failure reason

Unclear State Transition

Cancellation is allowed without checking the current payment state.

This may allow a refunded payment to become cancelled.

Improved Code

JAVA
@Service
public class RefundService {
    private static final Logger log =
            LoggerFactory.getLogger(RefundService.class);

    private final PaymentRepository paymentRepository;
    private final PaymentGateway paymentGateway;

    public RefundService(PaymentRepository paymentRepository,
                         PaymentGateway paymentGateway) {
        this.paymentRepository = paymentRepository;
        this.paymentGateway = paymentGateway;
    }

    @Transactional
    public RefundResult refundPayment(Long paymentId) {
        Payment payment = findPayment(paymentId);

        validateRefundEligibility(payment);

        String refundReference = executeRefund(payment);

        payment.setStatus(PaymentStatus.REFUNDED);
        payment.setRefundReference(refundReference);

        Payment savedPayment = paymentRepository.save(payment);

        return new RefundResult(
                savedPayment.getId(),
                savedPayment.getStatus(),
                savedPayment.getRefundReference()
        );
    }

    @Transactional
    public void cancelPayment(Long paymentId) {
        Payment payment = findPayment(paymentId);

        validateCancellationEligibility(payment);

        payment.setStatus(PaymentStatus.CANCELLED);
        paymentRepository.save(payment);
    }

    private Payment findPayment(Long paymentId) {
        if (paymentId == null) {
            throw new IllegalArgumentException("Payment ID is required");
        }

        return paymentRepository.findById(paymentId)
                .orElseThrow(() -> new PaymentNotFoundException(paymentId));
    }

    private void validateRefundEligibility(Payment payment) {
        if (payment.getStatus() != PaymentStatus.SUCCESS) {
            throw new InvalidPaymentStateException(
                    payment.getId(),
                    payment.getStatus()
            );
        }

        if (payment.getAmount() == null
                || payment.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidRefundAmountException(payment.getId());
        }
    }

    private void validateCancellationEligibility(Payment payment) {
        if (payment.getStatus() == PaymentStatus.REFUNDED) {
            throw new InvalidPaymentStateException(
                    payment.getId(),
                    payment.getStatus()
            );
        }

        if (payment.getStatus() == PaymentStatus.CANCELLED) {
            throw new InvalidPaymentStateException(
                    payment.getId(),
                    payment.getStatus()
            );
        }
    }

    private String executeRefund(Payment payment) {
        try {
            return paymentGateway.refund(
                    payment.getTransactionId(),
                    payment.getAmount()
            );
        } catch (PaymentGatewayException ex) {
            log.error(
                    "Refund failed for paymentId={}, transactionId={}",
                    payment.getId(),
                    payment.getTransactionId(),
                    ex
            );

            throw new RefundProcessingException(
                    payment.getId(),
                    ex
            );
        }
    }
}

Payment states are explicit:

JAVA
public enum PaymentStatus {
    PENDING,
    SUCCESS,
    FAILED,
    REFUNDED,
    CANCELLED
}

A structured refund response can be used:

JAVA
public record RefundResult(
        Long paymentId,
        PaymentStatus status,
        String refundReference
) {
}

Why These Changes Help

refundPayment() and cancelPayment() communicate the supported operations directly.

Magic operation codes were removed.

The payment lifecycle uses a type-safe enum.

Missing payments produce a meaningful exception.

Refund eligibility is explicit.

Cancellation eligibility is explicit.

The external payment call is clearly identified.

Gateway failures include operational context.

The response communicates meaningful data rather than "OK" or "FAIL".

Future developers can determine where to modify:

  • Refund eligibility
  • Cancellation eligibility
  • Payment gateway handling
  • Payment state transitions

without decoding one large generic method.

27. Interview Perspective

Maintainability is commonly evaluated in Java, Spring Boot, senior developer, and code-review interviews.

An interviewer may provide working code and ask:

  • Would you approve this code?
  • How would you make it easier for other developers to maintain?
  • What makes Java code maintainable?
  • Why are meaningful names important?
  • Why are boolean flags sometimes problematic?
  • When should you use an enum instead of a string?
  • Why is constructor injection preferred?
  • How should exceptions be handled?
  • How would you refactor a legacy service safely?
  • How do you prevent knowledge silos?
  • When does abstraction hurt maintainability?
  • How do you balance readability and performance?

A strong answer should discuss more than formatting.

Maintainability includes:

  • Naming
  • Responsibility boundaries
  • Domain modeling
  • Error handling
  • Dependencies
  • Testing
  • Side effects
  • Logging
  • Consistency
  • Refactoring safety

28. Interview Questions and Answers

Basic Question

Question: What does maintainable Java code mean?

Answer:

Maintainable Java code is code that another developer can understand, test, debug, and modify safely without needing extensive explanation from the original author.

It normally has:

  • Clear naming
  • Focused responsibilities
  • Explicit business rules
  • Predictable side effects
  • Meaningful error handling
  • Testable dependencies
  • Consistent domain modeling

Intermediate Question

Question: Why can boolean parameters reduce maintainability?

Answer:

Boolean parameters often hide different behaviors.

For example:

JAVA
processOrder(order, true);

does not explain what true means.

If the parameter changes major behavior, explicit methods are usually clearer:

JAVA
cancelOrder(order);
confirmOrder(order);

Boolean parameters are not always wrong, but reviewers should question them when they represent separate business operations.

Advanced Question

Question: How do you know when abstraction improves maintainability versus when it becomes over-engineering?

Answer:

An abstraction is useful when it hides a stable implementation detail or represents a real reusable concept.

It becomes harmful when developers must navigate multiple interfaces, factories, wrappers, and generic layers to understand a simple operation.

I would consider:

  • Whether multiple implementations actually exist
  • Whether the abstraction represents a meaningful domain concept
  • Whether it reduces duplication
  • Whether it makes testing easier
  • Whether future changes are reasonably expected
  • Whether the abstraction makes normal debugging harder

The simplest design that clearly supports the current requirements is usually preferable.

Scenario-Based Question

Question: You inherit a legacy Spring Boot service with 1,000 lines and almost no tests. How would you improve maintainability?

Answer:

I would avoid rewriting it immediately.

First I would:

  1. Understand public behavior.
  2. Identify critical production paths.
  3. Add characterization tests around current behavior.
  4. Improve names where safe.
  5. Identify duplicated rules.
  6. Extract focused methods incrementally.
  7. Replace important magic values with domain types.
  8. Make dependencies explicit.
  9. Improve error handling and logging.
  10. Move substantial independent responsibilities to appropriate collaborators only when justified.

I would keep each refactoring small enough to verify that business behavior has not changed accidentally.

Code-Review Question

Question: What would you review specifically for maintainability in a Pull Request?

Answer:

I would check:

  • Whether names explain intent
  • Whether methods have focused responsibilities
  • Whether business rules are visible
  • Whether status values use appropriate domain types
  • Whether dependencies are explicit
  • Whether errors are handled intentionally
  • Whether database or network side effects are visible
  • Whether logic is duplicated
  • Whether tests cover new behavior
  • Whether the implementation follows existing project conventions
  • Whether another developer could modify the feature later without relying on tribal knowledge

Real-Project Question

Question: How does maintainable code reduce production risk?

Answer:

Most production defects are introduced during changes rather than during the initial implementation.

Maintainable code makes the relationships between business rules clearer.

When a future developer changes one rule, they are less likely to accidentally modify another.

Clear code also improves review quality, testing, debugging, and incident response, which reduces the probability and impact of production defects.

29. Quick Rule to Remember

Write every important method as if the next person changing it has never spoken to you.

The code itself should explain:

  • Its purpose
  • Its business rules
  • Its important side effects
  • Its failure behavior

30. Final Takeaway

Maintainable Java code is code that remains understandable after the original implementation context has disappeared.

The goal is not maximum abstraction, minimum line count, or complicated architecture.

The goal is predictable production code that another developer can safely work with.

What the Developer Should Remember

  • Use names that communicate business intent.
  • Keep responsibilities focused.
  • Replace magic values with meaningful domain types.
  • Keep important business rules explicit.
  • Make dependencies visible.
  • Handle failures deliberately.
  • Keep side effects identifiable.
  • Use logging that helps production investigation.
  • Write tests around important behavior.
  • Avoid unnecessary complexity.

What the Reviewer Should Check

A reviewer should verify:

  • Can the code be understood without additional explanation?
  • Are names meaningful?
  • Are responsibilities clearly separated?
  • Are business states explicit?
  • Are failure cases defined?
  • Are important database and external calls visible?
  • Are dependencies testable?
  • Is business logic duplicated?
  • Are state transitions valid?
  • Can future requirements be added without modifying unrelated behavior?
  • Is the implementation consistent with the surrounding codebase?

What Should Be Avoided in Production Code

Avoid:

  • Cryptic names
  • Generic methods that perform several operations
  • Magic values
  • Boolean flags controlling unrelated behavior
  • Swallowed exceptions
  • Arbitrary string return codes
  • Hidden side effects
  • Unnecessary abstraction layers
  • Copy-pasted business rules
  • Large utility dumping grounds
  • Weak production logging
  • Code that depends on the original developer explaining how it works

A useful measure of maintainability is not:

How quickly did I write this?

A better measure is:

How safely can another developer understand and change this six months from now?

Production code becomes valuable when it remains understandable long after the original author has moved to another feature, another team, or another company.