Writing Maintainable Software
Code is read far more often than it is written — the practices that make programs survive contact with humans (including future you)
A working program is the start of the job, not the end. Real software lives for years and is read, modified, and extended by many people — often including a future version of yourself who has forgotten how it works. Maintainability is the property of code that survives this reality gracefully.
This page is a tour of the habits that produce maintainable Java. None of them require any new language features. They are entirely about taste.
Small methods
The single most reliable maintainability rule is: each method should do one thing, and have a name that says what.
A method that fits on your screen is easier to understand than one that doesn't. A method whose name reads like a sentence ("calculate discount," "load orders," "validate email") tells the reader what to expect before they read the body.
Compare two versions of the same logic.
Version B is longer. It is also much easier to:
- Read — each line says what it does.
- Test — each helper can be tested in isolation.
- Change — moving from a 10% discount to a 15% discount is a one-line change in a method named for that exact rule.
Names tell the story
Compare:
double t = c * 0.07; // what is t? what is c? what is 0.07?
double tax = price * TAX_RATE;Both lines compile to the same bytecode. Only one survives a code review. A good name is a tiny piece of documentation that can never be out of date (as long as you keep it accurate).
Habits:
- Variables and methods:
camelCase. - Classes:
UpperCamelCase. - Constants:
SCREAMING_SNAKE_CASE. - Booleans read as questions:
isReady,hasNext,canPay. - Avoid abbreviations except for very well-known ones (
id,url,http).
Single Responsibility
A class with a clear purpose is reusable. A class that "does everything" is impossible to test, change, or understand.
A useful sanity check: can you describe what a class does in one sentence without using the word "and"? If you need "and," the class probably has more than one responsibility and wants to be two classes.
You don't have to split early. But when you notice the "and" in your description, refactor toward focused classes.
DRY — but not over-DRY
"Don't Repeat Yourself." If the same logic appears in three places, a bug-fix has to be made three times. Extract it.
But: do not deduplicate things that merely look the same right
now. Two functions might both compute x * 0.07 for completely
different reasons; they will evolve independently. Premature
deduplication couples them and bites you later. The right
question is: would a change in one place always require an
identical change in the other?
Magic numbers
if (age >= 18) { ... } // 18 = what?
if (age >= LEGAL_AGE) { ... } // self-explanatoryReplace literal numbers (other than 0, 1, occasionally 2) with named constants. The constant says why, the number says only what.
Comment the why, not the what
// Bad: comment restates the code
i++; // increment i
// Good: comment explains intent the code can't show
// We start from index 1 because index 0 is the header row.
for (int i = 1; i < rows.length; i++) { ... }A good rule: if your reader can understand the what from the code, comment the why. If they can't understand the what, rename something.
Errors at the right layer
We saw in the exceptions chapter that broad catch (Exception e)
hides bugs. The maintainability angle is: each layer of your
program should handle exceptions it understands, and let the
rest bubble up.
A web controller might convert a BadInputException into a 400
response. A database layer might wrap SQLException into a
domain-specific RepositoryException. A general utility method
probably shouldn't catch anything.
A maintainability mini-checklist
- Methods are small and named for what they do.
- Classes have a single, describable responsibility.
- Names are pronounceable and meaningful.
- No magic numbers.
- No empty
catchblocks. - No dead code or commented-out blocks "in case we need them."
- Tests exist for behavior anyone could break (we'll cover testing in a follow-up course).
A worked refactor
The following code works. It's also a maintenance nightmare. Read it; then read the cleaned-up version below.
Same behavior. Same arithmetic. Vastly easier to change when next month the marketing team says "make shipping free above $40" or "tax is now 7.5%."
Why are small, well-named methods generally easier to maintain than large ones?
They use less memory
The compiler inlines them automatically
Each method reads like a sentence, can be understood in isolation, and can be changed without affecting unrelated logic
The JIT runs faster on small methods
Which of the following is the best candidate to be replaced with a named constant?
The literal 0 used to start a loop
The literal 1 used to add one to a counter
The literal 0.07 used to multiply a price for tax
The literal "\n" used in a print statement
A class describes itself as "the OrderManager: it parses orders, validates them, saves them, and emails the customer." What's the most accurate critique?
The name should be longer
It needs more methods
It is doing too many unrelated things — the word "and" in its description hints that it should be split into several focused classes
It should be made abstract
Writing software that other people (including yourself in six months) can change happily is a slow-built skill. The exercises in the rest of this course quietly practice it. Next we move from how to write code to what code — the classic algorithms and data structures that every programmer should at least recognize.
Debugging and Reasoning About Programs
The skills that separate frustrated beginners from professional engineers — reading stack traces, forming hypotheses, and using invariants
Basic Algorithms and Data Structures
The handful of classic patterns every programmer learns to recognize — linear and binary search, simple sorts, and the stack and queue