Categories
Software

Writing READMEs People Actually Read

Most READMEs are written for the person who wrote the code. That person already knows how the project works, what it depends on, and why it exists. They write the README as a formality and fill it with implementation details that are only interesting after you’ve already understood the project. Then a new team member opens it six months later, reads “this project implements a reactive event bus with configurable back-pressure,” and closes the tab.

A good README is written for someone who has never seen the project. It answers the questions they actually have, in the order they have them, without assuming context they don’t have. Here’s how to write one that people open twice.

Answer the “why” before the “what”

The first paragraph of a README should tell the reader what problem this project solves and why it exists. Not what it is technically – what it does for you.

Compare:

Bad: “notifier is a Node.js library that implements a pub/sub pattern with configurable delivery semantics.”

Better: “notifier sends real-time updates to connected browser clients when server-side data changes. Drop it into an Express app and replace polling with websocket push in about an hour.”

The second version tells me what I’m getting and roughly whether it’s worth reading further. The first one tells me the implementation approach, which I don’t need to know yet.

Structure: the five sections you always need

Every project README needs at minimum:

Quick start – the minimum steps to get a working instance running. This should work when copy-pasted by someone who has never heard of your project. If it takes more than ten commands, it’s not a quick start.

## Quick start

git clone https://github.com/your-org/notifier
cd notifier
cp .env.example .env
npm install
npm run dev
# open http://localhost:3000

Requirements – Node 20+, Postgres 15+, Redis. Be specific about versions. “latest Node” will cause problems when the next major version changes something.

Configuration – list every environment variable the application reads, what it does, whether it has a default, and what a valid value looks like. This is the section I refer back to most often when something isn’t working.

| Variable         | Required | Default   | Description                        |
|------------------|----------|-----------|------------------------------------|
| DATABASE_URL     | yes      | -         | Postgres connection string         |
| REDIS_URL        | yes      | -         | Redis connection string            |
| PORT             | no       | 3000      | HTTP port to listen on             |
| LOG_LEVEL        | no       | info      | debug, info, warn, error           |

Development setup – running tests, linting, database migrations, common development tasks. This is where the Makefile targets or npm scripts go.

Deployment – even one paragraph on how this gets to production. Is there a Docker image? A CI pipeline? Manual steps? The new person joining the team should not have to ask five colleagues how deployments work.

Code examples that actually run

Code examples in READMEs go stale fast. There are a few ways to fight this.

Source examples from actual test files rather than writing them inline. If the README says “see examples/basic.js“, that file is more likely to stay current because it’s part of the test suite. If the example lives only in the README, no one has a reason to update it when the API changes.

Mark language in fenced code blocks. Not just for syntax highlighting – it also signals to the reader what they’re looking at. An unmarked code block is ambiguous.

```bash
npm run build
```

```typescript
import { createNotifier } from 'notifier'
const n = createNotifier({ port: 3000 })
n.on('connect', (client) => console.log('client connected'))
```

Include the expected output for commands where it’s not obvious. “You should see something like:” followed by a truncated output block answers the implicit question “is this working?”

Keep it honest

Don’t document features that don’t work or aren’t finished. A README that says “supports clustering” when clustering is a stub function erodes trust faster than not mentioning it at all. Same goes for badges: a test coverage badge showing 94% on a project with three tests is misleading. Either keep badges accurate or remove them.

If there are known limitations, list them. “Does not support Windows” or “not recommended for more than 100 concurrent connections” saves someone from building on your project for a week before discovering it won’t work for their use case.

Keep it current

Stale documentation is worse than no documentation – it actively misleads people. A few practices that help:

Add a CI check that runs the quick-start commands in a clean environment. If the setup instructions fail in CI, they fail loudly before someone else hits them.

Date-stamp major sections that change infrequently: “Deployment (last updated 2026-03-01)”. It sets expectations and prompts periodic review.

Make the README easy to find and update. Keep it at the repo root. Make the first contribution guideline something like “if the README doesn’t match what you found, update it.”

Length and tone

A README is not documentation. Documentation is comprehensive; a README is a doorway into the project. It should be long enough to answer the first five questions a new reader has, and short enough that they’ll read the whole thing. For most projects that’s 500-1500 words – roughly what you’re reading now.

Write like you’re explaining to a smart colleague, not like you’re writing a spec. Active voice, short sentences, concrete examples. Avoid jargon unless the reader already knows it (if they’re looking at your project, they probably do).

The GitHub guide to README files is a useful baseline: docs.github.com – about readmes. And the Make a README project has good templates organized by project type: makeareadme.com.

The README is often the first thing someone reads before deciding whether to use, contribute to, or hire the person who built something. It’s worth an extra hour to get it right.

Categories
Software

Git Branching Strategies That Scale

I’ve worked in codebases with no branching strategy at all – everyone committing to main, hoping for the best – and I’ve worked in codebases with elaborate branching models that required a flowchart to understand. Neither extreme is good. The ideal branching strategy is the simplest one that keeps your main branch deployable and lets multiple engineers work in parallel without stepping on each other.

What that looks like in practice depends on your team size, release cadence, and whether you’re doing continuous deployment or scheduled releases. Here’s how I think about the common models and when each one earns its complexity.

Trunk-based development: the floor, not the ceiling

Trunk-based development (TBD) means everyone commits to a single branch (usually called main or trunk) frequently – ideally multiple times a day. Feature branches exist but are short-lived, measured in hours to a day or two at most. Long-lived feature branches are considered a smell.

This sounds chaotic if you haven’t tried it, but it’s the model behind how most high-performing engineering teams ship software. The discipline comes from feature flags and a test suite you trust. Incomplete features ship behind a flag that’s off in production; you merge the code before the feature is ready to show users.

# Example: simple feature flag check in application code
def render_new_dashboard(user):
    if feature_flags.is_enabled('new_dashboard', user_id=user.id):
        return render_template('dashboard_v2.html')
    return render_template('dashboard.html')

The payoff: integration problems surface immediately. You spend zero time resolving massive merge conflicts from a two-week-old branch. Your CI runs on code that reflects the actual state of the codebase rather than a parallel universe that diverged a week ago.

Where it breaks down: if your CI is slow (more than ten minutes to a green run), trunk-based development becomes painful. If you don’t have feature flags, half-finished work leaks into production. And if your team doesn’t have a culture of small commits, you end up with giant commits to main that are just as hard to review and revert as a big branch merge.

GitHub Flow: lightweight and honest

GitHub Flow is the simplest model that adds a review step. The rules are: main is always deployable; all work happens in a branch off main; branches are merged via pull request after review; merged code is deployed promptly.

git checkout main
git pull origin main
git checkout -b feature/OC-123-add-csv-export
# ... work ...
git push origin feature/OC-123-add-csv-export
# open PR, get review, merge, deploy

The branch naming convention doesn’t matter much – what matters is that branches are short-lived. I’ve seen “GitHub Flow” degenerate into branches that sit open for two weeks waiting for review, which removes most of the benefit. If PRs aren’t getting reviewed within a day, the bottleneck is process, not branching.

GitHub Flow is the right default for most teams shipping a web product continuously. It’s simple enough to explain in five minutes, integrates with every CI/CD system naturally, and doesn’t require ceremony.

Git Flow: when you need scheduled releases

Git Flow was introduced by Vincent Driessen in 2010 and became the dominant model for a while. It uses multiple long-lived branches: main (production), develop (integration), feature branches off develop, release branches that freeze features and allow only bug fixes, and hotfix branches off main for emergency patches.

This model makes sense when you have scheduled releases – say, a versioned SDK or a mobile app that goes through an app store review. The release branch gives you a stabilization period without blocking new feature work on develop.

It does not make sense for a web product that deploys multiple times a day. The overhead of maintaining two long-lived branches (main and develop), creating release branches, and merging hotfixes into both is real. I’ve seen teams cargo-cult Git Flow onto a continuous deployment web app and spend 20% of their git time on bookkeeping that adds zero value.

Branch naming and commit hygiene

Regardless of the model, a few conventions make git history actually useful:

Name branches by ticket ID and a short description: feature/OC-456-user-notifications or fix/OC-789-null-pointer-login. The ticket ID makes it easy to trace a branch to a requirement.

Write commit messages that explain the why, not the what. The diff shows what changed. “fix null pointer on login when user has no profile” is more useful than “fix bug”. The conventional commits spec (conventionalcommits.org) is worth reading – it formalizes a message structure that tooling (changelog generators, release scripts) can parse.

feat(auth): add magic link login for enterprise accounts

Adds a passwordless login flow using time-limited tokens.
Required by enterprise customers who cannot use OAuth providers.
Refs: OC-892

Squash merge vs. merge commit is a preference call. Squash gives you a clean history where each PR is one commit – good for git log --oneline readability. Merge commits preserve the full development history. I prefer squash for feature branches and merge commits for release branches.

Scaling to a larger team

Once a team grows past about fifteen engineers, a few things change. Code review becomes a bottleneck unless you have clear ownership (CODEOWNERS files help). Long-lived feature branches become more tempting because coordinating across teams on a fast-moving trunk is harder.

The answer isn’t a more complex branching strategy – it’s better tooling. Merge queues (supported natively in GitHub and GitLab) serialize competing PRs and run CI on the projected merged result before actually landing the commit. This eliminates a class of “passed CI on my branch but broke main” incidents that become more common as the team grows.

The Git documentation on branching is worth reading even if you’ve used Git for years – the mental model of what branches actually are (pointers to commits) clarifies a lot of edge cases: git-scm.com/book/en/v2.

Whatever model you pick, the most important thing is that the team understands it and applies it consistently. A mediocre branching strategy applied consistently beats a perfect one that people route around.