#What CI and CD actually mean
AddedContinuous integration (CI) is a team habit, not a tool: everyone merges their work into the shared main branch at least once a day, and every merge is checked by an automated build and test run. Martin Fowler's article "Continuous Integration" (revised 18 January 2024) defines it that way, and his list of practices includes "Everyone Pushes Commits To the Mainline Every Day", "Every Push to Mainline Should Trigger a Build" and "Fix Broken Builds Immediately" (after version control, build automation and self-testing builds). Having Jenkins or GitHub Actions installed is not CI; a team with a beautiful pipeline and two-week branches is doing something else.
Continuous delivery builds on that: main is always deployable, and any commit that passes the pipeline could go to production at the push of a button. Fowler's "ContinuousDelivery" bliki entry (30 May 2013) gives the test: a business sponsor could ask for the current development version to go live and nobody would panic. Continuous deployment goes one step further: every change that passes the pipeline goes to production automatically, with no human deciding. You need continuous delivery first; whether you then automate the final step is a business choice.
| Continuous integration | Continuous delivery | Continuous deployment | |
|---|---|---|---|
| What's automated | build and tests on every push to main | the whole path to a production-ready artefact, plus a one-click deploy | everything, including the production deploy |
| Who decides to release | (not about releasing) | a person or the business, any time | nobody: every green commit ships |
| What main must be | integrated daily and green | always deployable | always deployable, and deployed |
| Needs first | tests you trust, a fast build | CI, automated deploys, environments like production | continuous delivery, strong monitoring, fast rollback |
The mental model: integration is a bill you pay either way. Every change someone else makes to code you're also changing is a cost: you have to understand it, merge it and retest. Integrating daily is paying that bill in small instalments while the details are fresh. Integrating every few weeks is a balloon payment: the same changes, all due at once, when nobody remembers why they were made. The model below makes that concrete.
// Six developers change a 40-file codebase for 20 working days; each merges into main every N days.
// At each merge, a "clash" is a file this branch changed that main also changed since the branch last synced.
// Seeded PRNG (mulberry32): every strategy gets exactly the same edits, only the merge rhythm changes.
function mulberry32(seed) {
return () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const FILES = 40, DEVS = 6, DAYS = 20, EDITS_PER_DAY = 3;
const weights = Array.from({ length: FILES }, (_, i) => 1 / (i + 1)); // a few hot files (routes, shared types)
const total = weights.reduce((a, b) => a + b);
const pickFile = (rand) => {
let r = rand() * total;
for (let i = 0; i < FILES; i++) if ((r -= weights[i]) <= 0) return i;
return FILES - 1;
};
function simulate(every) {
const rand = mulberry32(42);
const pending = Array.from({ length: DEVS }, () => new Map()); // file -> day this branch first changed it
const mainSince = Array.from({ length: DEVS }, () => new Set()); // files main changed since this branch synced
let merges = 0, clashes = 0, worst = 0, ageSum = 0;
for (let day = 1; day <= DAYS; day++) {
for (const branch of pending)
for (let e = 0; e < EDITS_PER_DAY; e++) {
const f = pickFile(rand);
if (!branch.has(f)) branch.set(f, day);
}
if (day % every !== 0 && day !== DAYS) continue;
for (let d = 0; d < DEVS; d++) {
const clashing = [...pending[d]].filter(([f]) => mainSince[d].has(f));
merges++;
clashes += clashing.length;
worst = Math.max(worst, clashing.length);
for (const [, firstDay] of clashing) ageSum += day - firstDay + 1; // days the clashing edit waited
for (let o = 0; o < DEVS; o++) if (o !== d) for (const f of pending[d].keys()) mainSince[o].add(f);
pending[d].clear();
mainSince[d].clear();
}
}
return { merges, clashes, worst, age: ageSum / clashes };
}
console.log("merge every merges clashes/merge worst merge clash found after");
for (const every of [1, 5, 10, 20]) {
const r = simulate(every);
console.log(
`${(every === 1 ? "day" : `${every} days`).padEnd(11)} ${String(r.merges).padStart(6)} ` +
`${(r.clashes / r.merges).toFixed(1).padStart(13)} ${String(r.worst).padStart(11)} ${r.age.toFixed(1).padStart(10)} days`,
);
}merge every merges clashes/merge worst merge clash found after
day 120 1.4 3 1.0 days
5 days 24 7.5 10 3.4 days
10 days 12 11.8 18 6.6 days
20 days 6 16.5 25 13.5 days- Six developers make the same edits in every strategy (the PRNG is reseeded with 42 for each run): three file changes a day each, on a 40-file codebase where a few files are hot (file i is picked with weight 1/(i+1), like the router, the shared types and the API client in a real web app). Only the rhythm of merging changes.
- A clash is a file a branch changed that main also changed since the branch last synced. It's a proxy for a merge conflict or, worse, a silent semantic conflict that the tests have to catch.
pendingrecords the day each branch first touched a file;mainSincerecords what landed on main behind each branch's back. - Merging every day: 120 small merges, 1.4 clashing files each, never more than 3, and each clash is found the day it was written (1.0 days). The developer still remembers the change, and the other person is one message away.
- Merging every 5, 10 or 20 days: the number of merges drops, but each one carries more clashes (7.5 → 11.8 → 16.5 on average, up to 25 in one merge), and a clash now surfaces 3.4, 6.6 or 13.5 days after the edit. That's the "merge week" before a release that every long-branch team knows.
- The total amount of overlapping work barely changes; what changes is batch size and delay. That's the whole argument for CI: you can't avoid integrating with colleagues, you can only choose to do it while it's small and fresh. The same idea drives trunk-based development (see Trunk-based development vs GitFlow).
Fowler's 2024 revision is blunt that running a CI service on feature branches is not CI: the branch is being built, but it isn't being integrated with anyone else's work. He calls it "semi-integration". Pull requests are compatible with CI only when they're small and short-lived: open in the morning, reviewed and merged the same day (see Small pull requests and fast reviews).
The practices that make daily integration possible when a feature takes two weeks:
- Hide work in progress instead of keeping it on a branch: a feature flag that's off in production, a route nobody links to yet, a new API version nobody calls yet. Fowler lists "Hide Work-in-Progress" as one of the core practices.
- Branch by abstraction for large refactors: put an interface in front of the old implementation, build the new one behind it in small merged steps, switch over, delete the old one.
- Expand and contract for database and API changes: add the new column or field, migrate readers and writers, then remove the old one, each step deployable on its own (see Environments, approvals and deployment strategies).
On a device platform there's a useful split. The web UI and the cloud or on-device API can usually move towards continuous deployment. Software that ships to devices in customers' rooms typically goes out as versioned releases through staged rollouts, so it stops at continuous delivery: every build is releasable, and a person decides when. That's a reasonable answer in an interview, as long as you say which part you mean.
A team runs the full test suite on every push to every pull request. PRs usually stay open for six to nine days while features are finished, then merge after review. Main is green 95% of the time. Are they doing continuous integration?
Show answer
No. They're doing automated testing on branches, which is useful, but not continuous integration.
- Humble's first question: does everyone push to the shared mainline at least daily? Here, work reaches main every six to nine days, so the answer is no.
- The tests on each PR check the branch against main as it was when the branch was last updated. Two PRs that each pass can still break each other once both are merged, because neither was tested with the other.
- The integration-cost model above shows the consequence: with a batch of five to ten days, clashes surface days after the code was written, in bigger merges.
- What would make it CI: smaller PRs that merge within a day, unfinished features behind flags, and a merge queue or "require branches to be up to date" so each PR is tested with what's ahead of it.