Over the years, I’ve worked on all kinds of projects—big, small, chaotic, overengineered, “temporary,” allegedly well-structured, and everything in between. And yet one pattern has been hilariously consistent:
The documentation is always missing, outdated, or buried in some forgotten corner of Confluence guarded by a 2019 timestamp.
Every project claims to have documentation.
Every team promises they’ll keep it updated.
And every developer confidently says, “Don’t worry, I’ll write it later,” which, as we all know, is the documentation equivalent of saying “the cheque is in the mail.”
After watching this cycle repeat itself more times than I’d like to admit, I realized something simple but brutally true:
Documentation only survives if it lives as close to the code as possible—because developers (myself included) are lazy creatures.
And from that, another realization came - the main problem with outdated documentation is the fact that most teams don’t consider it as part of the main development cycle, leaving it for later or making due with a couple of pre-dev specs they already had somewhere in Confluence.
To write meaningful documentation, let’s first discuss what type of documentation there is for a software component:
Ways to Document Code (That Developers Might Actually Use)
Once you accept that documentation has to live close to the code, the next question is how to structure it.
In practice, I’ve found that good documentation isn’t a single artifact - it’s a small ecosystem of layers, each serving a different purpose and audience.
For that project I mentioned (written in Java + Spring Boot), we identified four main types of docs:
1. Client Specs
These are conventional documents usually compiled by a domain expert. Ideally, they’re rich with ubiquitous language and provide code-agnostic descriptions of business cases - in other words, everything an engineer needs to truly understand the problem at hand.
Unfortunately, in reality, these documents often end up stored somewhere in Confluence, shared with the team at the beginning of a project, and then almost never updated again.
Don’t get me wrong: these documents are important.
Extracting knowledge from domain experts should be a top priority for any engineering team. But specs evolve, deadlines stress everyone, and those original documents slowly but surely become an outdated foundation.
The knowledge of changing requirements, edge cases, and project pivots ends up living in the code - or worse! - in the heads of senior developers.
To address this, we stopped writing specs in random Word files or Confluence pages destined to be forgotten and moved them as close to the front line as possible: the code repository.
We began storing them as Markdown (.md) files inside the repo, usually in a docs/ folder.
In large microservice environments, these documents can even justify a dedicated repository linked as a Git submodule. Submodules are often overlooked, but they’re a powerful way to keep multi-repo ecosystems organized.
For further reading on submodules, I highly recommend this blog post - link.
2. README – The Front Door for Humans
If client specs are the encyclopedia, the README is the front door. And yet, it’s one of the most overlooked pieces of documentation in software projects.
Ironically, almost every version-control platform - GitHub, GitLab, Bitbucket - automatically renders the README right on the main project page. It’s the very first thing any developer sees when landing in the repository. Despite this, READMEs are often shockingly empty, painfully outdated, or filled with the dreaded “TODO: write documentation later,” which of course never happens.
A good README is not optional. It’s the single document most likely to actually be read because it requires zero clicks. And because it sits at the root of the repo, developers are more likely to update it while working on the code. It’s documentation that lives in the path of least resistance.
A well-written README should answer the most important questions a developer has within the first few minutes of encountering a project:
- What does this service/project/component do? - a concise purpose and context statement - no marketing fluff.
- How do I run it locally? - runtime instructions, environment variables, dependency setup, and any project-specific commands.
- How do I test it? - unit tests, integration tests, test data setup, and any test-related tooling.
- How is the project structured? - a short explanation of the directory layout, key modules, and where to find the important parts of the code.
- How do I deploy it? Deployment scripts, CI/CD notes, required permissions, or platform nuances.
- Where can I read further? - links to generated docs, architecture notes, API references, and centralised design documents.
The README is the gateway into the project. It sets expectations, reduces onboarding time, and gives every engineer—new or experienced—a clear orientation point. When maintained properly, it becomes the most valuable single document in the entire repository.
3. JavaDocs – Docs That The Compiler Annoys You Into Maintaining
JavaDocs - usually invisible, often forgotten, but absolutely essential when something breaks.
In practice, documenting Java projects often falls into two extremes: either over-commenting every trivial line, or no comments at all because “the code is self-documenting.” The truth, as always, is somewhere in the middle.
For our project, we made a conscious decision to document only what was genuinely necessary. That meant:
- using ubiquitous language so that domain terms matched exactly what was in the business specs
- explaining the why behind non-obvious decisions, not the what the code already reveals
- documenting core domain classes, interfaces, and complex workflows
- avoiding comment walls that simply repeat the method name in verbose form
JavaDocs, when used properly, serve as the micro-level documentation that clarifies intent without cluttering the code. And unlike Confluence pages, JavaDocs come with a built-in advantage:
- The compiler actually complains when they get out of sync.
If you rename a parameter but forget the JavaDoc tag, the build reminds you.
If you delete a method but leave stale comment references, the IDE lights up like a Christmas tree.
This gentle friction is exactly why JavaDocs tend to remain more accurate than other forms of documentation - they’re annoying to neglect.
To keep everything centralised and avoid “documentation islands,” we also used Doxygen.
Doxygen is a widely used documentation generator tool in software development. While primarily used in the C++ community, we used it to aggregate the JavaDocs into a consistent, browsable reference - an HTML page. It has a number of features that JavaDoc does not offer, e.g., the class diagrams for the hierarchies and the cooperation context, more summary pages, and many more.
This gave us a clean, unified view of the entire codebase without relying on scattered IDE hints.
4. Clean Code – The Documentation You Don’t Need to Maintain
Finally, the most underrated form of documentation is also the one developers respect the least until they really need it: clean code.
Comments age. Specs drift. READMEs get ignored. JavaDocs can become stale.
But well-written code—clear, intentional, and expressive—is documentation.
As Uncle Bob famously put it, “Clean code reads like well-written prose.”
And in practice, that means:
- classes and methods with names that reveal their purpose
- functions that do one thing and do it well
- domain terminology used consistently throughout
- no cleverness for its own sake
- no “surprise behaviors” hidden inside side effects
- no unnecessarily deep abstractions that require a PhD to understand
Clean code is the one artifact guaranteed to survive refactors, sprints, staff turnover, and organizational chaos. It’s always in sync with the behavior of the system because—it is the behavior of the system.
A simple rule emerged for us over time: If you need a paragraph of comments to explain a method, the real problem is the method.
When the code is expressive, everything else becomes easier → JavaDocs become lighter and more focused → the README shrinks because onboarding becomes obvious → specs become clearer because the domain model mirrors the business vocabulary.
Clean code doesn’t eliminate the need for documentation; it anchors it.
It ensures that all other forms of documentation—specs, READMEs, JavaDocs—are reinforcing the truth instead of compensating for unclear implementation.
In the end, clean code is the documentation that demands no maintenance, no CI integration, and no tooling. It’s the most honest, reliable explanation of what the system actually does.
Bringing It All Together — A Living Documentation Pipeline
After refining how we wrote documentation, the final step was to ensure it wasn’t just created—but continuously delivered and always accessible. For that, we built a documentation pipeline that turned our specs, README files, and JavaDocs into a single, unified living documentation portal.
Since we wanted our docs to live as close as possible to our code, we decided to deploy everything through GitHub Pages, which gave us an effortless, version-controlled hosting solution directly tied to the codebase. Github Pages is not particular to Github (GitLab, for example, also has GitLab pages) - it is another neat feature of the popular version control systems I recommend for further reading - link.
To compile everything into a set of cohesive pages, we used Sphinx.
Sphinx is a powerful documentation generator written in Python. It takes source files written in reStructuredText or Markdown and translates them into various output formats, including HTML, PDF, and ePub.
Sphinx handled the structure, navigation, and theming. Doxygen fed Sphinx with auto-generated JavaDocs using the Breathe plugin, giving us a fully browsable API reference straight from the source code. Meanwhile, the root README served as the immediate entry point for developers exploring the repository, linking seamlessly into the generated documentation site.
Every merge into main triggered a CI job that rebuilt the docs, ensuring the portal reflected the latest code changes, API updates, and design decisions. Nothing lived in isolation. Nothing required manual syncing. And crucially—nothing was more than a single click away.
This turned our documentation from a static artifact into a continuously updated ecosystem that developers actually used, because it was integrated directly into their daily workflow and lived right alongside the code.
Conclusion — Documentation Is Our Responsibility, Not Our Burden
At the end of the day, writing documentation isn’t optional. It’s part of our job as engineering professionals. Future teammates depend on it. Onboarding depends on it. Maintenance, reliability, and even architectural clarity depend on it. But accepting that responsibility doesn’t mean documentation has to be painful.
By keeping docs close to the code, automating whatever can be automated, and relying on smart tools like Markdown, JavaDocs, Doxygen, and Sphinx, we remove the friction that normally kills documentation efforts. We turn something that traditionally feels like a chore into a natural extension of everyday development. And when we pair that with clean code, good READMEs, and domain-rich specs, the entire ecosystem becomes self-sustaining.
Documentation doesn’t need to be perfect—it just needs to be accessible, accurate, and alive. When we build systems that encourage developers to update docs as easily as they write code, everyone benefits: the team, the product, and most importantly, the poor soul who inherits the project six months later.
Good documentation doesn’t make us less lazy. It just makes being lazy compatible with building great software.
