Clean Code and Maintainability Engineering


Engineering notes on code quality beyond naming and formatting, covering maintenance cost, modular decomposition, error contracts, testability, static analysis, concurrency, and change risk.
Clean-code discussions often begin with naming, indentation, and short functions. Those details matter, but the engineering problem is deeper: how quickly can an engineer who did not write the code form a correct model of its behavior, how narrowly can a change be contained, and how can we demonstrate that behavior was not broken?
Clean code is a maintenance-cost and change-risk problem rather than an aesthetic preference. Sources from the 2015 context and Java practices used in 2026 belong to different historical periods. The examples use modern Java syntax, but their purpose is to expose design decisions rather than teach Java.
Process models and requirements management belong to Software Engineering; verification and validation belong to Software Testing Engineering; vulnerability classes belong to Secure Software Engineering.
Unit 1: A Measurable Definition of Code Quality
Code quality after a change cannot be inferred from fewer lines alone. Identify the existing behavior to preserve and the tests that exercise it. Compare functional equivalence, execution cost, and debugging effort; justify a new abstraction only when it actually reduces change cost. A shorter function whose behavior becomes scattered across files has not necessarily become easier to maintain.
1.1. Quality Is a Set of Criteria, Not an Adjective
"Good code" is not a one-dimensional judgment. ISO/IEC 25010:2011 described product quality through eight characteristics. The second edition published in 2023 reorganized the model and defines nine product-quality characteristics. A 2015 discussion and the standard current in 2026 should therefore not be read as if they used exactly the same edition and terminology.
For clean-code work, maintainability is central. Correct output today is not enough; a system must also support safe modification, analysis, and verification.
"This code is dirty" is not measurable. "Changing this business rule requires modifying six classes and three configuration files" defines an engineering problem that can be investigated.
1.2. Maintenance Economics
Code is written relatively few times and read many times. A shortcut that saves minutes during initial implementation can impose repeated cognitive cost over years of maintenance. The reverse also matters: adding allocations, copying, or indirection to a hot path only for visual cleanliness can be a real cost.
The objective is therefore not the prettiest code. It is a defensible total engineering cost.
1.3. Metrics Guide Attention
Cyclomatic complexity, dependency count, file size, and churn are useful signals, but none directly measures whether a human understands a design.
Two functions can have the same complexity score while one naturally expresses the domain and the other is an accidental state machine. A metric identifies an area for inspection; engineering judgment explains why it is or is not justified.
1.4. Reading Code Quality Through Change Cost and Verifiability
Code quality is part of maintenance cost and change risk, not merely a style preference. Decomposition should follow the axis of change. A good boundary localizes a decision. Automation checks repeatable rules; review checks rationale.
Clean-code principles are contextual rather than absolute. Performance-sensitive, real-time, embedded, and safety-critical systems can place the tradeoff at different points. A deliberate exception should remain local and preserve the measured reason that justified it.
Distinctions worth preserving:
- Formatting is not the whole of readability.
- A short function is not proof of single responsibility.
- High test coverage is not proof of correct verification.
- Low cyclomatic complexity is not proof of understandable design.
- No static-analysis warning does not mean no defect.
- Many comments do not mean decisions are well documented.
- Refactoring is not a behavior change.
- A thread-safe primitive is not a thread-safe workflow.
- Dependency injection does not automatically produce correct dependency direction.
- Similar code is not always duplicated knowledge.
- Immutability reduces some defect classes but does not remove allocation cost.
- Virtual threads do not solve races or backpressure.
1.5. Measuring Maintainability Through Change
Code quality is not captured by naming and formatting rules alone. More concrete signals include how far a small business-rule change propagates, how many contracts it touches, and whether failure behavior remains predictable.
Behavior-preserving tests reduce refactoring risk. After a change, externally observable contracts such as dependency direction, performance, and error behavior should also be checked.
An abstraction is useful when it hides real variation. If a new layer cannot explain which duplication or change axis it reduces, it may add maintenance cost rather than remove it.
1.6. Change Locality and Behavioral Equivalence
A maintainable codebase keeps a small requirement change inside a small and predictable surface. If one behavioral fix requires touching many distant files, the problem is not merely line count; the behavior itself is distributed through the system.
Refactoring should therefore aim for smaller propagation of behavior, not simply more abstraction. A helper, layer, or shared component is useful when it genuinely concentrates a change axis in one place. Otherwise it can lengthen call chains and increase reading cost.
Behavioral equivalence also extends beyond the successful path. Error codes, keyboard behavior, ordering stability, timeouts, write counts, and authorization results may all be observable parts of the contract. Safe refactoring identifies those properties before structural change and checks them afterward.
before: behavior + evidence
|
v
structural change
|
v
after: same behavior + equal or stronger evidenceMixing unrelated cleanup into the same change enlarges the diff and makes regressions harder to localize. Dead code can be removed when it truly carries no behavior, but the decision should be supported by call, data, and test evidence rather than by the fact that the code looks old. This connects directly to staged modernization in Software Engineering.
1.7. Understandability as a Reliability Property
Security and reliability are not independent bolt-on features; both shape how a system contains faults and abuse. For maintainability, understandable code is necessary but insufficient; the number of components affected by a change, privilege boundaries, resource consumption, secure defaults, and rollback mechanisms should also be minimized so routine refactoring does not create a broad blast radius. In denial-of-service scenarios in particular, the protection mechanism itself must not consume the availability it is intended to preserve.
- Treat readability as the capacity to make safe changes, not merely as style.
- Keep critical invariants visible in naming and module boundaries.
- Aim for post-incident behavior to be reconstructable from source and operational evidence.
Unit 2: Reading Cost and Naming
2.1. A Name Is a Function of Scope
Identifier length should be considered together with visible scope. i can be perfectly clear in a three-line loop while d can be unacceptable as long-lived class state.
Problematic:
public List<User> get(final List<User> u) {
final List<User> r = new ArrayList<>();
for (final User x : u) {
if (x.status() == 1) {
r.add(x);
}
}
return r;
}A domain-oriented form:
public List<User> findActiveUsers(final List<User> users) {
final List<User> activeUsers = new ArrayList<>();
for (final User user : users) {
if (user.isActive()) {
activeUsers.add(user);
}
}
return activeUsers;
}The improvement does not come from ArrayList or the loop. It comes from removing the backward-navigation cost imposed by get, u, r, x, and status() == 1.
2.2. Searchability
A stable name for a meaningful concept reduces codebase-scale search cost. Raw values do not carry that property:
if (elapsedSeconds > 86400) {
archive();
}Naming the domain value exposes intent:
private static final long SECONDS_PER_DAY = 86_400;
if (elapsedSeconds > SECONDS_PER_DAY) {
archive();
}Not every literal is a magic number. The 1 in index + 1 already means "the next index" in many contexts; NEXT_INDEX_OFFSET would add noise rather than information.
2.3. One Concept, One Word
If the same operation is called load, fetch, and read in different areas, readers reasonably assume the words represent different contracts. If the behavior is truly different, different names are correct. If not, a shared vocabulary is cheaper.
Naming is not about using more words. It is about reducing incorrect branches in the reader's mental model.
Unit 3: Function Boundaries and Levels of Abstraction
Function boundaries should distinguish business rules that change together from details that change for different reasons. If extracting a block makes callers slightly shorter but greatly increases parameter and intermediate-state transfer, reconsider the boundary. Expose preconditions, side effects, and failure behavior. Useful decomposition is not maximizing function count; it keeps the consequences of a change predictable.
3.1. Responsibility Is Not Line Count
A function boundary is not determined by its number of lines. The stronger question is whether it operates at one coherent decision level.
Mixed responsibilities:
public void process(final Order order) {
if (order == null) {
throw new IllegalArgumentException("order");
}
if (order.total().signum() <= 0) {
throw new IllegalStateException("Order total must be positive");
}
repository.save(order);
final String email = order.customer().email();
if (email != null) {
mailService.send(email, "Order saved");
}
}Separating boundaries exposes the high-level flow:
public void process(final Order order) {
validate(order);
repository.save(order);
notifyCustomer(order);
}
private void validate(final Order order) {
if (order == null) {
throw new IllegalArgumentException("order");
}
if (order.total().signum() <= 0) {
throw new IllegalStateException("Order total must be positive");
}
}
private void notifyCustomer(final Order order) {
final String email = order.customer().email();
if (email != null) {
mailService.send(email, "Order saved");
}
}This is not a rule to make every function smaller. Dozens of micro-functions called once and understandable only from their call-site context can increase navigation cost.
3.2. Guard Clauses and Nesting
Deep nesting forces the reader to keep too many predicates active:
public void publish(final Media media) {
if (media != null) {
if (media.isReady()) {
if (!media.isDeleted()) {
publisher.publish(media);
}
}
}
}Guard clauses can reduce that active state:
public void publish(final Media media) {
if (media == null || !media.isReady() || media.isDeleted()) {
return;
}
publisher.publish(media);
}The goal is not fewer lines; it is lower cognitive state.
3.3. Flag Arguments
public void save(final User user, final boolean notify) {
repository.save(user);
if (notify) {
notificationService.send(user);
}
}save(user, true) does not explain behavior. If these are genuinely different workflows, name them:
public void save(final User user) {
repository.save(user);
}
public void saveAndNotify(final User user) {
repository.save(user);
notificationService.send(user);
}A call such as setEnabled(boolean enabled) is different: the Boolean directly represents state. The smell appears when the Boolean acts as a selector hiding separate workflows.
3.4. Command-query Separation
A method that looks like a query but mutates state creates a hidden side effect:
if (userRepository.existsAndMarkSeen(userId)) {
continueProcessing();
}If atomicity does not require one operation, the state transition can be made visible:
if (userRepository.exists(userId)) {
userRepository.markSeen(userId);
continueProcessing();
}When one database operation is required for latency or atomicity, the combined behavior can still be correct; the method name should make that contract explicit.
Unit 4: Comments as a Contract with Code
4.1. Comments Are Not Compiled
Comments are not automatically verified with the code. When implementation changes and prose does not, a stale comment can be more dangerous than no comment.
Weak comment:
// Continue if the user is active
if (user.status() == 1) {
process(user);
}Move the meaning into code:
if (user.isActive()) {
process(user);
}4.2. Rationale Can Belong in Comments
Code can show what happens but not always why a measured exception must remain:
// A 64 KiB block produced the lowest measured p99 write latency on this device family.
// Re-benchmark on the same storage profile before changing this value.
private static final int WRITE_BUFFER_SIZE = 64 * 1024;This comment does not repeat the code. It preserves the decision source and the condition for changing it.
4.3. Commented-out and Dead Code
With version control, long-lived commented-out code creates ambiguity without preserving unique information. The same applies to unreachable functions.
A TODO is useful when it contains a concrete exit condition:
// TODO: Remove this fallback after the provider's 2.4 timeout defect is no longer supported.TODO: fix carries no decision and no completion criterion.
Unit 5: Modular Decomposition and Information Hiding
This unit separates volatile implementation details from a stable core through explicit contracts so that changes remain localized.
stable core
|
v
abstraction / contract
|
v
volatile detail
database / protocol / file format / external API
may change while the core remains stable5.1. Hide the Volatile Decision
Parnas's criterion places module boundaries around design decisions likely to change rather than around execution steps. Ideally, changing one decision changes one localized area.
A business layer can depend on a stable boundary:
public interface ReportRepository {
List<ReportRow> load(final ReportRange range);
}
public final class ReportService {
private final ReportRepository repository;
public ReportService(final ReportRepository repository) {
this.repository = repository;
}
public List<ReportRow> generate(final ReportRange range) {
return repository.load(range);
}
}The interface itself does not create quality. Adding an interface for every class with one stable implementation and no observed change axis can be structural noise.
5.2. Coupling and Cohesion
A class that changes for unrelated reasons has low cohesion and often accumulates dependency directions. Dependency graphs can reveal such nodes, but high connectivity is not automatically bad: a stable central domain abstraction can legitimately have many edges.
5.3. The Cost of Premature Abstraction
Two fragments should not be merged merely because they look similar. If the actual axis of change has not appeared yet, a common base type can tie unrelated behavior to the wrong contract.
Abstract repeated knowledge, not merely repeated text.
5.4. Cognitive Load and Change Propagation
"Clean" code can become subjective, so maintainability is better evaluated through change propagation. How many modules, tests, and configuration files must change for one business rule? Duplicated representations increase coordination cost.
one requirement change
|
v
module A
/ \
v v
module B module C
| / \
v v v
test B test C integration
as fan-out grows, change cost and regression surface expand.More abstractions do not automatically mean cleaner design. Layers are valuable when they hide real variation; layers built for hypothetical future variation can increase navigation cost.
A strong maintainability signal is that a small requirement change remains small. Module boundaries, tests, and error contracts matter more than naming alone.
5.5. Reading SOLID, YAGNI, and Design Patterns Through Change Cost
Clean-code principles can become counterproductive when reduced to slogans. “Every class must do one thing,” “everything needs an interface,” or “professional code uses patterns” can increase class count while also increasing maintenance cost.
A more useful question is:
Which kind of change is this principle preventing from spreading?
5.6. Single Responsibility: Separate Axes of Change, Not Merely Class Size
The Single Responsibility Principle is not a rule that a class must have only one method.
A module that performs pricing, PDF rendering, email delivery, and persistence combines several independent reasons to change.
Separating domain policy, presentation, delivery, and persistence can improve change locality.
Conversely, splitting five tiny classes that always change together does not automatically improve maintainability.
5.7. Open/Closed: Not “Never Modify Code”
The Open/Closed Principle should not fill a system with abstractions for hypothetical futures.
Its useful form appears when there is evidence of a real variation axis and new behavior can be added without repeatedly rewriting a stable core.
A pricing-policy interface can be appropriate when pricing policies genuinely multiply. Creating interfaces, factories, and registries for one fixed policy merely because variation might appear one day is speculative generalization.
5.8. Liskov Substitution Is a Behavioral Contract
The Liskov Substitution Principle requires more than matching method signatures.
A subtype should preserve the observable contract expected through the base type. It should not arbitrarily narrow valid inputs, redefine successful output meaning, violate promised invariants, or introduce surprising side effects.
The deeper lesson is that inheritance must be justified by behavioral substitutability, not only by a verbal “is-a” relationship.
Composition can often make the behavioral dependency more explicit.
5.9. Interface Segregation: client-specific Contracts
The Interface Segregation Principle does not require every interface to be tiny.
The problem is forcing a client to depend on operations it never needs.
A useful interface groups behaviors that are consumed together and change together.
5.10. Dependency Inversion: Not Interface Multiplication
The Dependency Inversion Principle reduces direct dependence of high-level policy on low-level implementation details.
business policy
↓
port / contract needed by the policy
↑
database, HTTP, file adapterThis does not imply one IXxxService for every class.
If an abstraction creates no meaningful test boundary, technology boundary, or policy boundary, it may only lengthen the call chain.
5.11. SOLID Principles Form a Connected System
The principles can be read as a shared attempt to reduce uncontrolled change propagation:
SRP -> separates change axes
ISP -> reduces unnecessary client dependency
DIP -> separates policy from infrastructure
LSP -> protects behavioral reliability of abstractions
OCP -> supports proven variation axes5.12. YAGNI: Defer Unproven Generalization, Not Real Requirements
YAGNI distinguishes required flexibility from speculative flexibility.
required flexibility:
current requirement or strong evidence of change
speculative flexibility:
no evidence beyond "maybe someday"Speculative flexibility increases classes, configuration, tests, and failure paths.
YAGNI must not become an excuse to ignore security, migration, compatibility, or regulatory concerns that genuinely require up-front planning.
5.13. Patterns Are a Language of Forces and trade-offs
Factory, Strategy, Observer, Decorator, Adapter, and Command are useful because they name recurring design forces.
For each pattern, ask:
what problem exists?
which forces conflict?
which trade-off does the pattern choose?
which new cost does it introduce?Strategy makes a changing algorithm explicit but adds indirection.
Decorator supports behavioral composition but can lengthen call and failure traces.
Singleton may express single ownership in narrow cases but can also become global mutable state that couples tests.
A pattern name is not a justification.
5.14. Composition over Inheritance
Inheritance is powerful, but a subtype can become tightly coupled to base-class implementation and lifecycle.
Composition expresses:
Service
has-a Policyand can make changeable behavior a visible collaborator.
Composition also should not create unlimited interface chains. If behavior does not vary, direct code can remain clearer.
5.15. Signs of Pattern Overuse
The following can indicate that abstraction has become larger than the problem:
an interface with one implementation for years
factory chains for trivial construction
singletons that hide global state
strategy hierarchies for tiny fixed variation
mechanical repository/service layers per database table
pass-through classes created only to match an architecture diagramThese are not automatic defects, but they require a clear answer to: “Which change cost does this layer reduce?”
5.16. Refactoring versus Redesign
Refactoring improves internal structure while preserving observable behavior.
If a change modifies API contracts, error behavior, transaction boundaries, ordering, or user interaction, it includes behavioral redesign rather than pure refactoring.
This distinction changes the required review and regression-test scope.
5.17. A Practical Decision Sequence
Before adding a new abstraction, ask:
1. Is there duplicated knowledge, not just similar text?
2. Do these parts change for the same reason?
3. Do clients need the same contract?
4. Does the abstraction create a meaningful boundary?
5. Does inheritance preserve behavior safely?
6. How much indirection does the new layer add?
7. Which unproven future am I trying to model?
8. Can a simpler design provide the same change locality?Clean code is not code that applies the largest number of principles. It is code with clear intent, local change, verifiable behavior, and low unnecessary structural burden.
Unit 6: Error Handling and Error Contracts
This unit shows how infrastructure failures acquire context at a boundary and are translated into a stable failure contract for application policy.
infrastructure failure
|
v
boundary layer
|
+--> add context
+--> translate to a stable failure contract
|
v
application policy
|
+--> retry / user message / compensation / terminate6.1. Keep Failure Policy Explicit
Java exceptions can separate failure flow from normal results, but they create non-local control transfer. The important question is where a failure becomes meaningful.
Returning null can spread checks to every caller:
public Media load(final long id) {
return repository.findById(id);
}If "not found" is exceptional at this layer, the contract can say so:
public Media load(final long id) {
final Media media = repository.findById(id);
if (media == null) {
throw new MediaNotFoundException(id);
}
return media;
}If "no result" is an ordinary outcome, Optional, an empty collection, or an explicit result type may be better. The rule is not "replace every null with an exception."
6.2. Add Context Where Abstraction Changes
Low-level failures should not leak arbitrarily across boundaries:
try {
return jdbcRepository.load(id);
} catch (final SQLException e) {
throw e;
}A repository boundary can translate infrastructure detail into a repository contract:
try {
return jdbcRepository.load(id);
} catch (final SQLException e) {
throw new UserRepositoryException("Cannot load user " + id, e);
}Mechanically wrapping every exception adds no value. Context belongs where the abstraction level changes.
6.3. Never Swallow Failure Without Policy
try {
write(record);
} catch (final IOException ignored) {
}The failure has not disappeared; only its visibility has. Logging and continuing is also not automatically correct. The caller's ability to distinguish success from failure is part of the contract.
6.4. Partial State
When work stops halfway, the remaining state must be defined. Database transactions, atomic file publication, message idempotency, and compensation solve different forms of partial progress. Error handling is therefore a state-management problem, not merely an exception-class choice.
6.5. Exception Family, Physical Source, and Resolution Contract
The name of an exception class is not the same thing as the physical source of a failure. An application may use an exception named DataException for source-data integrity, an external-service response, an ORM safety rule, or a genuine database failure. Classifying all of these as “database errors” from the class name alone damages both the user message and technical diagnosis.
A more stable error model keeps several independent dimensions:
Exception family -> how the application treats the failure
Error code -> concrete stable failure type
Source -> layer where the problem originated
Cause -> missing, invalid, timeout, conflict, and similar form
Resolution -> next safe action
Resolver -> user, local administrator, source system, or technical teamA direct consequence is that classification logic should not be repeated in every controller or screen. The same JDBC connection failure can appear through many endpoints; separate interpretations eventually create divergent messages and behavior. If physical-source classification changes together, a central classifier or error contract is the natural change boundary.
Central classification does not require a new exception class for every condition. A small set of behavioral families combined with stable catalogued error codes can reduce maintenance cost compared with hundreds of tiny exception types. An explicit domain classification should take precedence over a generic cause-chain inference because the application may know more about the context than the infrastructure layer does.
Cause-chain inspection also does not require unbounded traversal. A bounded, cycle-safe traversal can locate JDBC, file, or network root causes only on failure paths without adding work to the normal hot path. Domain exceptions and infrastructure exceptions with similar names must remain distinct as well: an application-level record conflict and a database deadlock do not have the same remedy.
The user-facing error contract and the technical logging contract should remain separate. The user can receive a safe source, cause, and resolution while technical logs retain the real exception chain. The security implications are discussed in Secure Software Engineering, while the verification implications connect to Software Test Engineering.
Unit 7: Boundary Code and External Dependencies
7.1. Prevent External Models from Leaking Inward
If a provider DTO circulates through the entire application, a provider field change becomes a core change.
A boundary can localize the dependency:
public interface PersonnelGateway {
User findById(final String id);
}public final class RestPersonnelGateway implements PersonnelGateway {
private final RestClient restClient;
public RestPersonnelGateway(final RestClient restClient) {
this.restClient = restClient;
}
@Override
public User findById(final String id) {
final ExternalUser response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(ExternalUser.class);
return new User(response.id(), response.name(), response.department());
}
}The core never needs to know ExternalUser.
7.2. Do Not Wrap Every Dependency
A small stable standard library does not automatically need an application-specific wrapper. The value of a boundary is to localize volatility and replacement pressure.
7.3. Learning Tests
Documentation and observed behavior are not the same thing. Small tests can encode assumptions about a third-party library and reveal whether an upgrade changes those assumptions.
Feathers's seam concept serves a related purpose in legacy code: find a substitution point before attempting broad restructuring.
7.4. The Build Dependency Graph Is Part of Software Architecture
Dependencies between classes in source code and dependencies between libraries in a build system are different graphs. Both influence runtime behavior and change risk.
The real dependency surface of an application is larger than the dependencies declared directly:
application
↓
direct dependency
↓
transitive dependency
↓
further transitive dependenciesA node in that graph can change while application source code remains identical and still affect build behavior, runtime classpath, serialization, HTTP clients, logging infrastructure, bytecode instrumentation, security behavior, or native integration.
Dependency management is therefore not merely a package-resolution problem delegated to the build tool. It is part of maintainability engineering.
7.5. Direct and Transitive Dependencies Express Different Ownership
When application code directly uses the API of a library, that library is effectively part of the application contract.
Depending on an API only because another library happens to bring it transitively can create a hidden dependency:
application
↓
library A
↓
library B
application ---------> B APIThe application now depends on B, but the build definition may not express that ownership.
If a later version of A stops depending on B, application compilation or runtime behavior can break even though the intended A upgrade looked unrelated.
The useful rule is to make dependency ownership explicit when application code directly owns the API contract. This does not mean declaring every transitive artifact directly.
7.6. The Dependency Graph Is a Small Software Supply Chain
Inspecting only Java source files does not completely describe a production artifact.
An engineer should be able to determine why an artifact exists, whether it is direct or transitive, which path introduced it, which version was actually resolved, how competing version requirements were resolved, why an exclusion exists, and why a platform-managed version was overridden.
This visibility supports maintainability and diagnosis as well as security.
7.7. A BOM Is More Than a List of Versions
A platform such as Spring Boot can manage a coordinated set of dependency versions through a Bill of Materials.
The engineering effect is to reduce many independent version decisions toward one platform compatibility baseline.
This does not prove that every dependency combination is correct for every application. An application can still require an override.
The difference is that an override becomes a deliberate compatibility exception rather than an arbitrary version line.
An override should therefore have a reason and a verification boundary where practical.
7.8. Version Convergence Is Not Proof of Semantic Compatibility
A build system successfully selecting one version only proves that dependency resolution completed.
The classpath can be technically resolvable while runtime compatibility remains wrong.
Failure may appear at compile time, startup, the first execution of a specific path, a specific data shape, or only under production load.
Dependency convergence therefore does not replace integration testing or verification of the actual usage path.
7.9. Prefer the Smallest Explainable Dependency Surface
An unused dependency is more than disk space.
Adding one dependency can expand classpath, startup scanning, auto-configuration possibilities, native-image reachability, CVE monitoring surface, license surface, the upgrade graph, and collision probability.
Mechanically excluding every transitive dependency is not a better design.
The objective is not minimum dependency count but the smallest explainable dependency surface that satisfies the requirement.
7.10. A Starter Does Not Make Its Dependency Graph Irrelevant
A starter can group dependencies for a coherent capability and reduce repetitive configuration.
That improves development efficiency, but the capabilities brought onto the classpath should still be understood.
In Spring Boot, the classpath is not only compiler input. It can also participate in auto-configuration decisions.
Adding a dependency can therefore make a new API available and change the runtime configuration graph at the same time.
7.11. An Exclusion Is an Architectural Decision
When a transitive dependency is excluded, the reason should remain explicit: which parent introduced it, whether that parent expects it at runtime, which implementation replaces it, and whether a future upgrade can invalidate the assumption.
An unexplained exclusion list can age into maintenance debt in exactly the same way as unexplained legacy source code.
7.12. A Dependency Upgrade Is a Code Change Even with No Source Diff
A library version change can alter behavior without changing application source.
Potentially affected areas include default configuration, serialization, timeout behavior, retry semantics, SQL generation, logging, thread usage, reflection, validation, security defaults, and performance characteristics.
A successful build is therefore weak evidence for a significant dependency upgrade.
A stronger verification chain is:
dependency diff
↓
release notes / migration notes
↓
affected application paths
↓
integration / regression tests
↓
performance measurement where relevant7.13. Dependency Diffs Belong in Code Review
A change can modify only a few lines in a build file while changing a much larger resolved graph.
Tools such as Maven dependency:tree and Gradle dependency reports are diagnostic instruments. Their purpose is to reveal why a dependency path exists; they do not make the engineering decision themselves.
7.14. Build Reproducibility and Dependency Management Converge
If the same source revision resolves to different dependency versions six months later, the resulting artifacts can have different behavior.
The desired relationship is:
source revision
+ build configuration
+ dependency resolution
+ toolchain
=
reproducible artifactDependency management is therefore part of configuration management.
7.15. A Practical Maintainability Review
A dependency change can be reviewed in this order:
1. Which requirement does this dependency satisfy?
2. Is it direct or transitive?
3. Which version is actually resolved?
4. Does it override the platform or BOM?
5. Why?
6. Can the classpath change alter auto-configuration?
7. Which additional artifacts change?
8. Which API or runtime behavior may change?
9. Which verification protects that assumption?
10. What capability disappears if the dependency is removed?The objective is to keep the reason and cost of change visible inside the build graph as well as in source code.
Unit 8: How Testability Pressures Design
8.1. Testing Difficulty Is Often a Design Signal
If one unit test requires a real database, filesystem, and network, the problem is not only the test harness. Dependencies are embedded in the behavior.
Embedded construction:
public final class ReportService {
private final Repository repository = new OracleRepository();
private final Clock clock = Clock.systemUTC();
public Report create() {
return repository.load(LocalDate.now(clock));
}
}Supplying dependencies externally makes behavior controllable:
public final class ReportService {
private final Repository repository;
private final Clock clock;
public ReportService(final Repository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
public Report create() {
return repository.load(LocalDate.now(clock));
}
}The test can freeze time:
final Clock clock = Clock.fixed(
Instant.parse("2026-09-08T09:00:00Z"),
ZoneOffset.UTC
);
final ReportService service = new ReportService(repository, clock);This does not imply that every helper needs injection. Wrapping stable side-effect-free utilities can create more design cost than test value.
8.2. Tests Should Protect Behavior
A test coupled to implementation detail can break during refactoring even when behavior is unchanged. The long-term value of a test is that it preserves externally meaningful behavior while internal structure evolves.
One of TDD's durable design effects is that it forces the interface to be considered from the caller's perspective.
Unit 9: Measurement, Static Analysis, and Automation
9.1. Tool Boundaries
Static analysis inspects code without running it. Depending on language and analyzer it can detect unused values, some resource leaks, suspicious control flow, type problems, and risky language constructs. It cannot by itself prove domain correctness or a p99 latency target.
9.2. Cyclomatic Complexity
McCabe's metric for a single connected control-flow graph is:
V(G) = E - N + 2Its value is as a branching signal, not an automatic quality judgment.
A domain decision can sometimes be collected into one named location:
public boolean isProcessable(final Media media) {
return media.status() == MediaStatus.ACTIVE
&& media.durationSeconds() > 0
&& !media.deleted();
}The method does not magically remove complexity. It localizes one decision.
9.3. Automate Repeatable Rules
During my Baykar internship I worked on programming standards and built a source-code analyzer that automated part of the rule set. It did not rely on naive text search; it processed the source character by character and distinguished comments, strings, preprocessor text, and ordinary code.
The same character sequence can mean different things in different lexical states. A comment delimiter inside a string is not a comment, and an assignment operator in a condition requires context. I later normalized findings from cppcheck and splint into the same result model. The durable lesson was simple: a deterministic rule should not depend on human memory alone. Historical details are in the Baykar software internship record.
9.4. Code Review
Once formatting and mechanical checks are automated, review can focus on rationale: why the change exists, which alternative was rejected, which boundary cases were considered, whether dependency direction is correct, and where new risk was introduced.
A performance improvement cannot be established from a single runtime observation. Setup overhead, JIT compilation, filesystem cache state, processor frequency, and background contention all influence timings. Compared revisions require the same workload, an invariant correctness check, and repeated independent measurements. Reported average speedup should be accompanied by dispersion, outliers, and an acceptable latency tail. Shorter code is not an engineering improvement when correctness or latency stability is degraded.
Unit 10: The Limit of Readability in Concurrent Code
10.1. A thread-safe Primitive Is Not a thread-safe Workflow
Mutable shared state is the central cost of concurrent code. Even a counter can race:
private long processed;
public void markProcessed() {
processed++;
}AtomicLong makes an individual increment safe:
private final AtomicLong processed = new AtomicLong();
public void markProcessed() {
processed.incrementAndGet();
}But a compound decision can still race:
if (processed.get() < limit) {
processed.incrementAndGet();
processItem();
}Each individual operation is thread-safe while the invariant "never exceed the limit" spans both calls. Synchronization must protect the invariant, not merely the primitive.
10.2. Reduce Sharing
The cheapest lock is the lock you do not need. State local to a task requires no synchronization. Immutable objects also reduce mutation races after publication.
10.3. Lock Scope
Large critical sections simplify reasoning but increase contention. Tiny sections can reduce contention while accidentally splitting one invariant. The boundary should be chosen from correctness first and validated with profiling and load tests.
Readable concurrent code is not code that merely looks smooth. Ownership, invariants, and synchronization boundaries must be visible.
Unit 11: Rules Depend on Context
11.1. Performance-sensitive Code
Abstraction layers, intermediate objects, and collection transformations can create real cost on hot paths.
For example:
public List<Sample> positiveSamples(final List<Sample> samples) {
return samples.stream()
.filter(sample -> sample.value() > 0)
.toList();
}can be clear and appropriate in ordinary application code. In an allocation-sensitive path processing millions of samples, in-place work or preallocated buffers can be better.
The rule is measurement, not ideology. Sacrifice readability only where a measured hot region justifies it.
11.2. Real-time and Embedded Constraints
Worst-case execution time, queue bounds, blocking behavior, and allocation policy can matter more than average throughput. In such systems, making resource bounds visible is part of readability.
11.3. Historical Context
A strange decision in old code may reflect the compiler, hardware, and runtime constraints of its period. Before removing an old optimization, identify the original constraint.
Unit 12: Formatting as a Visual Contract
12.1. Vertical and Horizontal Structure
Formatting is more than aesthetic. Related expressions should appear related, and separate concepts should be visually separated.
Dense form:
if(user!=null&&user.isActive()&&rights.contains(user.id())&&!blocked.contains(user.id()))process(user);Visible boundaries:
if (user != null
&& user.isActive()
&& rights.contains(user.id())
&& !blocked.contains(user.id())) {
process(user);
}Multi-line layout is not always better. A short natural expression can remain on one line. The goal is not to worship formatter output; it is to make relationships visible.
12.2. Team Consistency
One mechanically enforced format across a codebase is cheaper than several individually good styles. Formatter and linter issues should not consume the main code-review discussion.
Unit 13: Objects, Data Structures, and the Location of Behavior
13.1. Private Fields Are Not Sufficient Abstraction
A class that exposes direct getters and setters for every field can still leak representation.
Anemic account:
public final class Account {
private BigDecimal balance;
public BigDecimal getBalance() {
return balance;
}
public void setBalance(final BigDecimal balance) {
this.balance = balance;
}
}Behavior-oriented account:
public final class Account {
private BigDecimal balance;
public Account(final BigDecimal balance) {
this.balance = balance;
}
public void withdraw(final BigDecimal amount) {
if (amount.signum() <= 0) {
throw new IllegalArgumentException("amount");
}
if (balance.compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}
public BigDecimal balance() {
return balance;
}
}The withdrawal invariant remains inside the object.
13.2. A DTO Can Be Intentionally Behaviorless
The same rule should not be applied mechanically to transfer types:
public record PersonnelResponse(
String id,
String name,
String department
) {
}Its job is to carry data. Not every data structure should become a rich domain object. Objects and data structures optimize different kinds of change.
13.3. The Law of Demeter
This chain teaches the caller internal topology:
final String city = order.customer().address().city().name();If the behavior belongs to the domain object, a narrower contract can be used:
final String city = order.deliveryCity();Fluent APIs and plain DTO traversal are not automatic violations:
request.header("Accept", "application/json")
.timeout(timeout)
.send();The issue is not the number of dots. It is the amount of internal representation knowledge required from the client.
Unit 14: Separating System Construction from System Use
14.1. Construction and Use Are Different Responsibilities
Business logic that constructs infrastructure dependencies couples policy to mechanism:
public final class ReportService {
private final Repository repository = new OracleRepository();
private final Clock clock = Clock.systemUTC();
}External construction separates those decisions:
public final class ReportService {
private final Repository repository;
private final Clock clock;
public ReportService(final Repository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
}A composition root assembles concrete components:
final Repository repository = new OracleRepository(dataSource);
final Clock clock = Clock.systemUTC();
final ReportService reportService = new ReportService(repository, clock);Spring or another DI container can automate this wiring. Dependency Injection and Dependency Inversion are not the same concept. A container does not automatically repair incorrect dependency direction.
14.2. Cross-cutting Concerns
Transactions, authorization, logging, and telemetry can cut across many classes. Interceptors, proxies, and aspects can localize policy but can also create invisible runtime behavior. In critical systems, traceability of side effects matters as much as removing duplication.
14.3. Dependency and Configuration Engineering in Spring Applications
Clean-code principles in a framework-based application cannot be evaluated only by reading classes and methods. In a runtime such as Spring, object construction, dependency resolution, conditional bean creation, proxy chains, and external configuration are part of executable behavior.
As this invisible layer grows, maintainability becomes more than a source-code reading problem:
source code
+ bean graph
+ configuration
+ conditional activation
+ proxy behavior
+ runtime environment
=
actual application behaviorReadability in a framework-based system therefore includes the ability to understand the object graph and the boundaries at which behavior is added.
14.4. Bean Count Is Not a Quality Metric
A large bean count is not automatically a design defect. A large system can legitimately contain many components. The stronger question is whether each component has a clear reason to exist and whether dependency direction is explainable.
A dependency graph should usually follow the direction of application flow:
Controller
↓
Application Service
↓
Domain / Repository / GatewayThe more significant warning appears when responsibilities point back to one another:
A
↓
B
↓
C
↓
AThis cycle raises a design question more important than bean-construction order. If A cannot make sense without B, B without C, and C without A, are the responsibility boundaries in the right places?
A circular dependency is therefore not merely a container-startup problem to be worked around. It can indicate mutual ownership of decisions, misplaced orchestration, or components that actually belong to the same change boundary.
The correction is not automatically another interface. In one system the right answer may be an explicit workflow component above two peer services. In another it may be moving one decision to its real owner or making data flow unidirectional.
14.5. Constructor Injection Makes Prerequisites Visible
A mandatory dependency supplied at construction time helps ensure that the object starts life in a valid state:
@Service
public final class ReportService {
private final ReportRepository repository;
private final Clock clock;
public ReportService(final ReportRepository repository, final Clock clock) {
this.repository = repository;
this.clock = clock;
}
}The engineering value is broader than annotation style. The constructor itself exposes what must exist before the class can perform its work.
When prerequisites are acquired through hidden field injection, global lookup, or runtime service location, an object can appear constructible while still depending on invisible state. Static reasoning, test setup, and dependency-graph analysis then become more expensive.
Constructor parameter count is not a mechanical quality threshold either. A large number of mandatory collaborators is an inspection signal that the class may own too much behavior, not an automatic instruction to split it.
14.6. Configuration Creates a State Space
If behavior changes while source code remains identical, configuration is an input to the executable system.
Values that appear independent can combine into materially different runtime states:
Spring profile
environment variable
feature flag
conditional bean
database target
timeout
queue capacity
security policyThree independent Boolean switches already produce eight theoretical combinations. Ten produce 1024. Real configuration values are rarely all Boolean, so the state space can grow much faster.
Every new profile or feature flag therefore has an engineering cost. Supported combinations, invalid combinations, production-safe defaults, obsolete flags, and duplicated configuration decisions should remain explicit.
Settings that share one lifecycle and validation boundary can be represented as one typed contract:
@ConfigurationProperties(prefix = "app.media")
public record MediaProperties(
Duration requestTimeout,
int batchSize,
int queueCapacity
) {
}The objective is not to wrap every property in another abstraction. It is to make one coherent configuration decision explicit and validatable.
14.7. Ownership at the auto-configuration Boundary
Spring Boot auto-configuration can construct infrastructure according to the classpath, existing beans, and application properties. This removes repetitive wiring.
Automatic behavior, however, should not become unquestioned behavior.
When an application replaces or modifies a default component, the design should be able to explain which default is replaced, why it is replaced, when the new bean is active, when the default returns, whether multiple candidates can exist, and whether a framework upgrade can invalidate the assumption.
Defining everything manually and delegating everything to defaults are both extremes. A more maintainable boundary is to explicitly own decisions that belong to the application while avoiding duplicate configuration for mechanisms the framework already provides reliably.
14.8. A Proxy Boundary Is an Invisible Behavioral Boundary
Some Spring AOP features execute a call through a proxy:
caller
↓
Spring proxy
↓
transaction / authorization / telemetry advice
↓
target objectA direct self-invocation made from inside the target object does not necessarily traverse that same proxy.
This matters for proxy-based behavior such as @Transactional. The presence of an annotation in source code does not by itself prove that the expected advice chain applies to the actual call path.
The maintenance risk is broader than a missing transaction. An engineer reading the source can form a runtime model that differs from what the application executes.
Framework-added behavior should therefore be treated as part of the design. When transactions, retries, authorization, caching, or telemetry change the contract of a critical method, the proxy boundary and invocation path should remain inspectable.
14.9. Abstraction Does Not Remove Physical Cost
A repository call can occupy one source line:
repository.findAllByStatus(status);The runtime path can still include SQL generation, connection-pool waiting, network round trips, database execution, entity materialization, and serialization.
Likewise, one annotation can trigger bean-graph processing, proxy creation, or runtime metadata.
Clean code therefore does not mean reducing visible source at any cost. The physical work hidden by an abstraction must remain recoverable when diagnosis or optimization requires it.
The physical performance model is developed further in High-Performance Java Data Systems.
14.10. Questions for Spring-oriented Code Review
A Spring change is often better reviewed through behavioral boundaries than through class size or formatting alone:
- Is dependency direction one-way and explainable?
- Are mandatory collaborators visible at construction time?
- Does a new setting represent a real behavior axis?
- Is default behavior safe and understood?
- Under which conditions does a bean exist?
- Does the call cross the expected advice chain?
- Does the transaction boundary correspond to the real unit of work?
- Can the runtime decision be reconstructed?
- Which assumption can change with the framework version?
The objective is not to produce more framework structure. It is to keep the invisible behavioral surface as small and explainable as practical.
Unit 15: Simple Design and Successive Refinement
15.1. The First Correct Solution Need Not Be the Final Design
Behavior can first be made correct and verifiable, then improved through small transformations. A broad rewrite mixes behavioral debugging with structural change in one diff.
15.2. Repeated Text Is Not Always Repeated Knowledge
Two calculations can encode the same business rule:
final BigDecimal invoiceTax = invoiceTotal.multiply(new BigDecimal("0.20"));
final BigDecimal refundTax = refundTotal.multiply(new BigDecimal("0.20"));The decision can be named:
private static final BigDecimal VAT_RATE = new BigDecimal("0.20");
public BigDecimal calculateVat(final BigDecimal amount) {
return amount.multiply(VAT_RATE);
}But if two similar formulas belong to different regulations or product policies and evolve independently, merging them because the lines look alike creates the wrong abstraction.
15.3. Four Practical Priorities
A useful ordering is: keep behavior verifiable, reduce repeated knowledge, make intent expressive, and keep unnecessary structure low.
The last priority must not defeat the others. Mixing responsibilities to reduce class count is as mechanical as creating an interface and factory for every class.
Unit 16: Code Smells as Diagnostic Signals
16.1. A Smell Is Not a Defect Verdict
Long parameter lists, flag arguments, feature envy, dead functions, repeated conditions, and oversized classes are inspection signals. Context determines whether they create actual change cost.
16.2. Feature Envy
A method that spends most of its effort reading another object's state may live in the wrong place:
public BigDecimal discount(final Customer customer) {
if (customer.orders().size() > 20
&& customer.totalSpend().compareTo(VIP_LIMIT) > 0) {
return VIP_DISCOUNT;
}
return BigDecimal.ZERO;
}If the policy belongs to the customer's lifecycle:
public BigDecimal discount() {
if (orders.size() > 20 && totalSpend.compareTo(VIP_LIMIT) > 0) {
return VIP_DISCOUNT;
}
return BigDecimal.ZERO;
}can be more local. If discount policy is a separate marketing rule that changes independently, moving it into Customer increases coupling instead. The smell does not decide the move.
16.3. Centralize Boundary Conditions
Equivalent end-of-range logic written differently can drift:
if (index < items.size() - 1) {
index++;
}if (selected + 1 < items.size()) {
selected++;
}If these are truly the same rule:
private boolean hasNext(final int index, final int size) {
return index + 1 < size;
}can centralize it.
16.4. Hidden Temporal Coupling
If start() is invalid before initialize(), do not leave that requirement only in documentation. A constructor, factory, or explicit state model should make invalid call sequences difficult or impossible where practical.
Unit 17: Execution Models and Shutdown in Concurrent Systems
This unit treats bounded queues, backpressure, worker completion, and controlled shutdown as parts of one producer-consumer execution model.
producer ---> bounded queue ---> consumer
| | |
| +--> backpressure|
| |
stop request -----------------------+
|
v
stop intake -> apply queue policy -> finish workers -> close resources17.1. Concurrency Is Not Synonymous with Speed
Concurrency separates what is done from when it is done. I/O-heavy workloads can overlap waiting and improve throughput. CPU-bound workloads can instead suffer from contention, context switching, and cache pressure.
Modern Java virtual threads can reduce thread cost for large numbers of I/O-heavy tasks:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (final Task task : tasks) {
executor.submit(() -> process(task));
}
}This does not solve races. Virtual threads change the cost model of execution units; mutable shared state, backpressure, and invariants remain design problems.
17.2. Producer-consumer and Backpressure
If producers outrun consumers, an unbounded queue only converts latency into memory growth. Queue capacity, rejection, slowing, or load-shedding policy must be part of the design. "Asynchronous" does not mean "unbounded."
17.3. Shutdown Is a Protocol
A service that starts but cannot stop cleanly has an incomplete lifecycle:
public final class Worker implements AutoCloseable {
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public void submit(final Runnable task) {
executor.submit(task);
}
@Override
public void close() {
executor.shutdown();
}
}Real shutdown may require more than shutdown(): stop accepting work, finish or cancel in-flight tasks, preserve interruption, drain queues, and close resources within a defined time.
17.4. Concurrent Testing Is Not Proof
A concurrency test that passes once does not prove correctness. Different worker counts, repeated stress, scheduler pressure, and multiple platforms increase the number of interleavings explored.
A rare unexplained failure should not automatically be dismissed as "flaky." It can be evidence of a race. Stress testing is still not formal proof; it increases the probability of exposing failure.
References
- Apache Maven Dependency Plugin, dependency:tree: https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html
- Barbara H. Liskov; Jeannette M. Wing. A Behavioral Notion of Subtyping. ACM TOPLAS, 16(6), 1994. DOI
- Brian W. Kernighan; P. J. Plauger. The Elements of Programming Style, 2nd ed. McGraw-Hill, 1978.
- David L. Parnas. On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12), 1972. DOI
- Donald E. Knuth. Literate Programming. The Computer Journal, 27(2), 1984. DOI
- Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3), 1968. DOI
- Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
- Frederick P. Brooks. The Mythical Man-Month. Addison-Wesley, 1975.
- Heather Adkins; Betsy Beyer; Paul Blankinship; Piotr Lewandowski; Ana Oprea; Adam Stubblefield. Building Secure & Reliable Systems: Best Practices for Designing, Implementing, and Maintaining Systems. O’Reilly Media, 2020.
- International Organization for Standardization. ISO/IEC 25010:2011 — Systems and software Quality Requirements and Evaluation (SQuaRE). ISO, 2011. URL
- International Organization for Standardization. ISO/IEC 25010:2023 — Systems and software engineering — Systems and software Quality Requirements and Evaluation (SQuaRE) — Product quality model. ISO, 2023. URL
- Kent Beck. Test-Driven Development: By Example. Addison-Wesley, 2002.
- Martin Fowler. Refactoring: Improving the Design of Existing Code, 2nd ed. Addison-Wesley, 2018.
- Meir M. Lehman. Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE, 68(9), 1980. DOI
- Michael C. Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004.
- Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008.
- Robert C. Martin. The Clean Coder: A Code of Conduct for Professional Programmers. Prentice Hall, 2011.
- Spring Boot, Build Systems and Dependency Management: https://docs.spring.io/spring-boot/reference/using/build-systems.html
- Spring Boot, Developing Auto-configuration: https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html
- Spring Boot, Managing Dependencies: https://docs.spring.io/spring-boot/gradle-plugin/managing-dependencies.html
- Spring Boot, Properties and Configuration: https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html
- Spring Framework, Declarative Transaction Implementation: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html
- Spring Framework, Proxying Mechanisms: https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
- Steve McConnell. Code Complete, 2nd ed. Microsoft Press, 2004.
- Thomas J. McCabe. A Complexity Measure. IEEE Transactions on Software Engineering, SE-2(4), 1976. DOI