Skip to content

SE 15 Foundational Coding Principles

Dr M H B Ariyaratne edited this page Jun 9, 2026 · 1 revision

SE-15: Foundational Coding Principles

Part of the Software Engineering Principles series


Overview

Beyond formal methodologies and architectural patterns lie a set of time-tested principles that guide day-to-day coding decisions. These principles are language- and framework-agnostic. They are not rigid rules — professional engineers sometimes consciously violate one for a good reason — but you should always know why you are doing so and document the trade-off.


1. KISS — Keep It Simple, Stupid

Favour simple solutions over complex ones.

Complexity is cumulative. Every unnecessary abstraction, clever trick, or over-engineered class adds to the cognitive load of every future reader and maintainer. Simple code is easier to understand, test, debug, and extend.

In practice:

  • Solve the actual problem in front of you, not the imagined harder version.
  • Prefer a straightforward if/else over a strategy pattern when there are only two cases.
  • Resist the urge to make code "flexible" for scenarios that do not yet exist.

Warning sign: When you find yourself proud of how clever a piece of code is, that is usually a signal to simplify it.


2. YAGNI — You Aren't Gonna Need It

Don't add functionality until it is actually needed.

YAGNI is a direct consequence of KISS applied to features. Speculative generality — code written "just in case" — is among the most common sources of unnecessary complexity and technical debt.

In practice:

  • Write the smallest implementation that satisfies the current requirement.
  • Do not add configuration flags, extension points, or abstract base classes for hypothetical future callers.
  • If a need arises later, add the feature then — with real requirements in hand.

Why it matters: Code that is never executed still needs to be read, tested, and maintained. It also misleads future readers about what the system actually does.


3. DRY — Don't Repeat Yourself

Every piece of knowledge must have a single, unambiguous, authoritative representation in the system.

Duplication is not just a style problem — it is a reliability problem. When the same logic exists in two places, every bug fix or requirement change must be applied twice. One copy will eventually be forgotten.

In practice:

  • Extract repeated logic into a shared method, class, or module.
  • Consolidate duplicate constants into a single named value.
  • Use inheritance, composition, or templates to avoid repeating structural patterns.

Caution: DRY applies to knowledge, not just text. Two functions that happen to look similar but represent different concepts should not be merged. Premature deduplication creates accidental coupling between unrelated things.


4. SOLID Principles

SOLID is covered in depth in SE-04: SOLID Principles. A brief summary for reference:

Letter Principle Core idea
S Single Responsibility A class should have one reason to change
O Open-Closed Open for extension, closed for modification
L Liskov Substitution Subtypes must be substitutable for their base types
I Interface Segregation Many client-specific interfaces beat one general-purpose interface
D Dependency Inversion Depend on abstractions, not concretions

5. Separation of Concerns

Different parts of the system should handle different responsibilities.

A system that mixes UI rendering, business logic, and data access in the same place is difficult to test, reuse, or change independently. Separating these concerns makes each part smaller, more focused, and independently replaceable.

Classic layers:

  • Presentation — renders output, captures input (JSF pages, REST controllers)
  • Business logic — enforces rules, orchestrates workflows (EJBs, service classes)
  • Data access — reads and writes persistent state (JPA repositories, DAO classes)

In practice: A JSF backing bean should not run SQL. A JPA entity should not format currency. A service method should not build HTML strings.


6. Encapsulation

Hide internal details and expose only what is necessary.

Encapsulation protects the invariants of an object. When internal state is freely accessible and mutable from anywhere, any piece of code can put an object into an invalid state, and tracking down the source of corruption becomes extremely difficult.

In practice:

  • Make fields private by default; expose only what callers legitimately need.
  • Provide mutators only when mutation is intentional, and validate within them.
  • Return copies of mutable collections from getters when the internal collection must not be modified externally.

7. Abstraction

Hide complexity behind well-defined interfaces or APIs.

Abstraction lets you reason about a system at the right level without drowning in irrelevant detail. A well-chosen abstraction boundary means callers never need to know how something is done — only what it does.

In practice:

  • Design method signatures that express intent (processPayment(Bill b)) rather than implementation (callPostgresToInsertRowAndUpdateBalance(...)).
  • Use interfaces to define contracts; let implementations vary behind them.
  • Regularly ask: "At what level of abstraction am I working, and is this the right level for this problem?"

8. Modularity

Break the system into cohesive, loosely coupled modules.

A monolithic tangle of classes that call each other freely is impossible to test in isolation, difficult to assign to different team members, and painful to refactor. Modular design divides the system into units with clear boundaries and well-defined interfaces.

Benefits:

  • Modules can be developed, tested, and deployed independently.
  • Teams can work in parallel without constant merge conflicts.
  • A module can be replaced or rewritten without touching the rest of the system.

9. High Cohesion, Low Coupling

Cohesion measures how related the responsibilities within a single module are. A highly cohesive class does one thing well.

Coupling measures how much a module depends on other modules. Loosely coupled modules communicate through stable interfaces and are unaware of each other's internals.

Target: High cohesion within modules; low coupling between them.

High coupling Low coupling
High cohesion Fragile but focused ✅ Ideal
Low cohesion Worst case ("god class") Spread-out, hard to follow

Achieving this combination usually means applying SOLID principles, Separation of Concerns, and thoughtful interface design together.


10. Write Clean Code

Clean code is covered in depth in SE-06: Clean Code. The core ideas:

  • Meaningful names — a good name removes the need for a comment.
  • Small, focused functions — each function does one thing.
  • Clear intent — code should read like well-written prose.
  • Minimal nesting — flatten conditionals and early-return instead of deep nesting.
  • No surprises — functions should do exactly what their name suggests.

Code is read far more often than it is written. Optimise for the reader.


11. Boy Scout Rule

Leave the codebase cleaner than you found it.

Coined by Robert C. Martin, the Boy Scout Rule says that whenever you touch a file, make a small improvement — rename an unclear variable, extract a duplicated block, delete dead code, add a missing test. You are not committing to a large refactoring; you are making incremental, continuous progress.

In practice:

  • Fix the one confusing name you see while implementing a feature.
  • Extract the one copy-pasted block you notice while reviewing a colleague's PR.
  • Delete the one commented-out block that has clearly been dead for years.

Over time, these small acts of stewardship keep a codebase healthy without ever needing a heroic "clean-up sprint."


12. Fail Fast

Detect errors as early as possible.

A system that fails fast surfaces problems immediately at their source. A system that swallows errors and continues produces corrupted state that only manifests much later — far from the root cause — making diagnosis extremely difficult.

In practice:

  • Validate method inputs at the point of entry; throw if they are invalid.
  • Use assertions to document and check invariants that must always hold.
  • In distributed systems, fail immediately on missing configuration rather than proceeding with defaults that may cause data loss.

Contrast: A null pointer exception on line 3 of a service method is far easier to diagnose than an NPE in a report that was built from data incorrectly inserted six steps earlier.


13. Defensive Programming

Validate inputs, handle errors gracefully, and never trust external systems.

Related to Fail Fast but broader: defensive programming assumes that callers will pass bad data, networks will fail, external APIs will return unexpected shapes, and databases will have constraint violations. It builds these assumptions into every layer.

In practice:

  • Validate all input at system boundaries (user input, REST payloads, file imports, external API responses).
  • Never assume a foreign key will resolve to a valid entity — handle the not-found case explicitly.
  • Use timeouts on all external calls; do not let a slow dependency block your own threads indefinitely.
  • Log errors with enough context to reconstruct what happened.

Distinction from over-engineering: Defensive programming applies to external boundaries. Within a single well-tested module, excessive null-checking and guard clauses add noise without safety.


14. Principle of Least Astonishment

Code should behave in the way most users and readers expect.

Also called the Principle of Least Surprise. A function named getTotal() that also sends an email violates this principle. A method named findById() that throws an exception when the record does not exist surprises callers who expect null or Optional.

In practice:

  • Name things accurately — if a method modifies state, its name should indicate that.
  • Follow the conventions of the language and framework. Diverging from standard idioms requires justification.
  • Avoid hidden side effects in getters, constructors, or comparators.
  • Keep the public contract of a component stable; changing observable behaviour is a breaking change regardless of whether the signature changed.

15. Automation

If it is repetitive, script it.

Manual processes are slow, inconsistent, and error-prone. Any task performed more than once by a human — running tests, building an artefact, deploying, formatting code, generating reports — is a candidate for automation.

Key areas:

  • Testing — automated test suites run on every commit via CI
  • Build — Maven, Gradle, or similar tools produce artefacts deterministically
  • Deployment — GitHub Actions, Jenkins, or similar pipelines deploy automatically on merge
  • Formatting/linting — static analysis catches issues before review
  • Database migrations — scripted, versioned schema changes applied automatically

Why it matters: Automation is a force multiplier. A 10-minute manual deployment done 20 times a week is 3.5 hours of toil. Automated in a pipeline, it is zero.


16. Test-Driven Development (TDD) / Testing-First Mindset

Testing is covered in depth in SE-09: Testing Principles. The TDD core idea:

Write the test before the code.

  1. Write a failing test that specifies the desired behaviour.
  2. Write the minimum code to make the test pass.
  3. Refactor — clean up the code without breaking the test.

Tests are not just verification; they are executable specifications. A test suite that passes is a living document of what the system is supposed to do. It also provides a safety net that makes refactoring safe.


17. Continuous Integration / Continuous Delivery (CI/CD)

Agile delivery practices including CI/CD are covered in SE-14: Agile and Scrum.

In brief:

  • Continuous Integration (CI): Every developer's changes are merged to a shared branch frequently (at least daily). Automated tests run on every merge to detect integration problems immediately.
  • Continuous Delivery (CD): The main branch is always in a deployable state. Any version can be released at any time with minimal manual intervention.

Why it matters: Long-lived feature branches accumulate divergence that makes merging painful. Frequent integration keeps the gap small. Automated pipelines eliminate the "works on my machine" class of problems.


18. Version Control Everything

If it is not in version control, it does not exist.

Code is the obvious candidate, but the principle extends further:

  • Configuration — application settings, server configuration, environment variables (not secrets)
  • Database schemas — migration scripts, not ad-hoc ALTER statements run manually
  • Infrastructure — Dockerfiles, Kubernetes manifests, Terraform definitions (Infrastructure as Code)
  • Documentation — architecture decision records, API docs, runbooks

Version control provides a complete audit trail, enables rollback, and allows the team to collaborate on any of these artefacts using the same workflows used for code.


19. Immutability (When Possible)

Prefer data structures that cannot be modified after creation.

Mutable shared state is the root cause of most concurrency bugs. An immutable object can be safely shared across threads, passed freely to untrusted code, and used as a dictionary key without fear of the underlying value changing.

In practice (Java):

  • Declare fields final when they should not change after construction.
  • Return unmodifiable views from collection getters: Collections.unmodifiableList(...).
  • Prefer value objects (no setters, all state set in constructor) for domain concepts like Money, Period, PatientId.
  • Use String, LocalDate, BigDecimal — all immutable by design.

Trade-off: Immutability sometimes requires more object creation. Profile before deciding this is a performance problem.


20. Observability — Logging, Metrics, Tracing

You cannot fix what you cannot see.

A system deployed to production with no observability is a black box. When something goes wrong — and it will — the only tool available is guesswork.

Three pillars:

Pillar What it answers Examples
Logs What happened, in what order Application logs, audit trails, error stacks
Metrics How healthy is the system right now Request rate, response time, error rate, queue depth
Traces How did this request flow through the system Distributed tracing across services

In practice:

  • Log at the right level: DEBUG for development detail, INFO for significant events, WARN for recoverable problems, ERROR for failures requiring attention.
  • Include enough context in log messages to reconstruct the user's action (user ID, operation, relevant IDs — never passwords or PII).
  • Instrument key operations from day one; retrofitting observability into a complex system is painful.

21. Security by Design

Security is covered in depth in SE-12: Security Principles.

The foundational idea: security is not a feature added at the end of development. It is a property designed into every layer from the beginning.

Core habits:

  • Validate and sanitise all input at every system boundary.
  • Apply the principle of least privilege — grant the minimum access a component needs, no more.
  • Never store passwords in plain text; never log credentials, PII, or secrets.
  • Keep dependencies up to date; known vulnerabilities in libraries are a primary attack vector.
  • Treat threat modelling as part of design, not an afterthought.

22. Performance as a Feature

Optimise only when necessary, and with measurements.

Premature optimisation — writing complex, hard-to-read code to solve a performance problem that has not been measured — is one of the most common sources of unnecessary complexity. As Donald Knuth observed: "Premature optimisation is the root of all evil."

The right process:

  1. Make it work correctly.
  2. Make it readable and maintainable.
  3. Measure — identify the actual bottleneck with profiling data.
  4. Optimise the bottleneck only.

However: Performance considerations that are structurally baked in (choosing the right algorithm, avoiding N+1 queries, using appropriate indexes) belong at design time. These are not premature — they are correct design.


23. Concurrent and Parallel Thinking

Design for concurrency when needed; understand what happens when two things happen at once.

Modern systems handle many requests simultaneously. Code that is correct when called by a single thread may produce race conditions, corrupted state, or deadlocks when called concurrently.

Core concepts:

  • Race condition — the result depends on the relative timing of two concurrent operations.
  • Deadlock — two threads each wait for a resource held by the other; neither can proceed.
  • Thread safety — a component is thread-safe if it behaves correctly when called from multiple threads simultaneously.

In practice:

  • Prefer stateless components (EJBs, CDI @RequestScoped) — no shared mutable state means no race conditions.
  • Use synchronisation primitives (synchronized, Lock, Atomic*) only when sharing state is unavoidable, and keep the critical section as small as possible.
  • Use database transactions to enforce atomicity at the persistence layer.
  • Understand that @SessionScoped and @ApplicationScoped beans may be accessed by multiple threads concurrently.

24. Documentation Is Part of the Code

Good code is self-documenting, but some things still need explanation.

The goal is not to eliminate documentation but to put each type of knowledge in the right place:

What to document Where
What a public API does Javadoc on public methods
Why a non-obvious decision was made Inline comment at the point of decision
Architecture and cross-cutting concerns Architecture Decision Records (ADRs)
How to set up, build, and run README / developer guide
Business workflows Wiki / user documentation

What not to document: What the code does — that is the code's job. A comment that says // increment counter above count++ adds noise, not knowledge.

Keep docs up to date: Stale documentation is worse than none — it actively misleads. Update docs as part of every change that affects them.


25. Feedback Loops

Shorten the cycle: code → test → deploy → monitor → learn.

Software development improves through rapid feedback. The longer it takes to discover that something is wrong, the more expensive it is to fix.

Types of feedback loops (fastest to slowest):

Loop Trigger Typical delay
Static analysis / linting On save in IDE Seconds
Unit tests On every build Seconds to minutes
Integration / CI tests On every commit Minutes
Code review Before merge Hours to days
Staging environment testing Before release Hours to days
Production monitoring / alerting After deploy Minutes to hours
User feedback After release Days to weeks

In practice: Invest in fast inner loops. A test suite that takes 45 minutes discourages developers from running it. A CI pipeline that fails clearly in 5 minutes prevents problems from reaching production.


Summary: Mottoes Many Engineers Live By

These sayings capture the spirit of the principles above:

"Make it work, make it right, make it fast." (in that order) — Kent Beck

Write something that is correct first. Clean it up second. Optimise only if measured performance requires it.


"If it hurts, do it more often." — Jez Humble

Painful activities like deployments, merges, or production rollouts hurt because they are infrequent. Doing them more often — and automating them — forces you to remove the pain rather than tolerate it.


"Code is a liability; the less you have, the better."

Every line of code must be read, tested, debugged, and maintained. The best code is code you did not need to write. Solve the problem with the minimum code that is correct.


"There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors." — Phil Karlton (adapted)

A lighthearted reminder that naming is genuinely difficult and deserves care.


How These Principles Interact

These principles are not independent rules — they form a coherent philosophy:

  • YAGNI prevents the features that would violate KISS.
  • DRY applied carelessly violates Separation of Concerns — deduplicate knowledge, not accidental similarity.
  • High Cohesion is what SOLID's Single Responsibility Principle looks like at the module level.
  • Fail Fast and Defensive Programming work together: validate at the boundary, fail immediately, log with context.
  • Automation, CI/CD, and short Feedback Loops are the same idea at different scales.
  • Immutability is the architectural expression of Encapsulation applied to data over time.

No single principle is absolute. A senior engineer applies them in context, knows when to violate one deliberately, and can articulate why.


Previous: SE-14: Agile and Scrum

Back to Software Engineering Principles

Clone this wiki locally