Why Integration Should Be a Non-Event

For decades, the default experience of integrating code from multiple developers was defined by a large, windowless warehouse I once toured during a summer internship. In that room, programmers who had long finished writing their individual units were stuck in a months-long integration phase, with no one able to say when it would end. That specific failure mode has largely disappeared, but integration friction is still very real. A developer can pull changes from the main branch, spend days building a feature in a branch, and then watch a significant change land on main that forces her to rework her code before she can even submit a pull request. The review process can then drag on while she context-switches to debugging unfamiliar code, and the whole ordeal discourages the team from ever refactoring core code again — allowing cruft to accumulate silently.

This is not an inevitable cost of collaboration. On well-run projects, integration is treated as a non-event. Any single developer's work is never more than a few hours away from a shared state, and merging it back takes minutes. When something breaks, it is discovered and fixed quickly. The tooling that enables this is neither exotic nor expensive: the core practice is simply that every developer on the team integrates their work into a controlled source code repository at least once a day. This practice is known as Continuous Integration, sometimes called Trunk-Based Development.

Continuous Integration is often misunderstood. Many teams say they are doing it, but an examination of their workflow reveals they have missed essential pieces. A clear definition helps everyone know what to expect and what else can be improved. The practice has only grown more valuable since the original conversations around it in the early 2000s, and it remains a core discipline for reducing delivery risk and keeping a codebase healthy enough for rapid feature work.

A Typical Workflow

A concrete example makes the mechanics clear. Suppose a team is extending a potion quality system to calculate how long a flying potion's effects last, adding logic that accounts for the moon phase during secondary mixing. The feature spans product code and automated tests.

The developer starts by pulling the latest mainline sources into a local environment with git pull. Before making any changes, she runs the full build command — this compiles the sources, starts the product, and runs a comprehensive test suite. The build takes only a few minutes and rarely fails, but it is worth running anyway: if she starts making changes on top of a failing build, any failure she later sees is harder to attribute to her own edits.

With a green baseline, she works on the moon-phase logic, running the build and tests frequently as she alters code and updates test coverage. After an hour or so the feature is ready to integrate back into the mainline. She pulls again to pick up any commits her colleagues made while she was working, then merges her changes on top of theirs and runs the build once more.

This time the build fails. The test output is indicative, but looking at the commits she just pulled is more illuminating. One of her colleagues refactored a function, moving logic into its callers and fixing all existing call sites. She had added a new call in her own feature, one her colleague could not have seen. She applies the same adjustment, reruns the build, and passes. A few minutes later she pulls again, sees another new commit, confirms the build is still green, and is able to git push her change.

Her push is not the final step. The mainline push triggers a Continuous Integration service, which checks out the changed code onto a dedicated CI agent and builds it there from scratch. The local build already passed, so a failure is unlikely — but rare is not the same as never, and "works on my machine" is a familiar phrase for a reason. The CI build takes long enough that the developer does not start a new task immediately. When the notification confirms all is well, she proceeds to the next part of the change.

The Difference Between Local Verification and Integration

Some teams believe they practice Continuous Integration merely because they keep a central repository and run tests in a CI service. That is a necessary foundation, not the practice itself. The value comes from the frequency of merging to the mainline and from treating that merge as the primary point of verification. If a developer's work exists in a branch for many days before it is merged, the integration errors accumulate and the essential feedback loop — finding conflicts while they are still small — is lost.

The goal is that any member of the team can integrate several times a day and experience that as a cheap operation. This requires a fast, reliable automated build. It also requires a willingness to give up long-lived feature branches, because those are precisely what reintroduces the integration pain that the warehouse of the 1980s so vividly demonstrated.

How Continuous Integration Works in Practice

A Continuous Integration workflow rests on a handful of core practices that together keep the mainline healthy and make integration painless. Each practice reinforces the others, and skipping any one of them undermines the whole approach. Here is what the day-to-day discipline looks like.

Track Everything in a Mainline Repository

Version control is now universal, but teams don't always exploit it fully. The real test is whether you can walk up to a machine with only a vanilla operating system, clone the repository, and build and run the product. That means the repository must give you — not necessarily store — the source, tests, database schema, test data, configuration files, install scripts, third-party libraries, and any tools required for the build.

There's a meaningful difference between storing and returning. You needn't keep the compiler in the repo, but you must be able to get the right compiler for the code you check out. If you're building last year's sources, the build has to fetch last year's compiler, not this year's. The repository can achieve this by referencing immutable asset storage, where an id always returns exactly the same artifact. The same applies to library code: only ever reference a specific version, never "the latest." This scheme also handles large assets like videos, letting build scripts fetch only what a particular build needs rather than pulling the entire repo.

A good rule is to store in source control everything you need to build anything, but nothing you actually build. Keeping build products in the repository signals a deeper problem — usually an inability to reliably recreate builds. Caching build products is fine as long as they are treated as disposable and removed promptly.

Two more details matter. First, prefer text files for anything defining the product and its environment. Version control systems handle binary files, but they rarely give you meaningful diffs, and without clear diffs you lose the ability to understand what changed. Second, keep a clear mainline: that single, shared branch that represents the current state of the product. In git this is usually called main, sometimes trunk or the older master. Developers commit to a local copy and push to the central repository's mainline, which moves many times a day in a CI environment.

Automate the Entire Build

Source code doesn't become a running system by itself. Compilation, file moves, schema loads, and the rest are exactly the kind of simple, repetitive tasks computers handle better than people. Tools like make have automated builds for decades, and modern environments all have their successors. Whatever tool you use, its instructions must be stored in the repository as text so they can be inspected and diffed. Tools that require clicking through UIs to build or configure an environment are incompatible with Continuous Integration.

Build automation tools earn their keep because they support a dependency network of tasks. The "test" task might depend on the "compile" task; invoking test first checks whether compile needs to run, which itself cascades down its own dependencies. This saves serious time when nothing has changed since the last run. The most common way to decide whether a task must run is to compare modification times: if any input file is newer than the output, the task executes.

Make sure the build is complete. It must include pulling the database schema from the repository and starting it in the execution environment. The goal: on a clean machine, check out sources, issue a single command, and have a running system.

Large systems build up finely tuned graphs of dependencies to minimize work. This website, for instance, has over a thousand pages; changing a single page rebuilds only that page, while altering a core toolchain file rebuilds everything. In both cases the command is identical — the build system decides how much to do.

Make the Build Prove Itself

A build that compiles and links is not a build that works. Untested code is a liability when you integrate several times a day; manual testing simply cannot keep pace with that frequency, and bug fixes against a rapidly changing codebase are brutally hard. The answer is a comprehensive automated test suite that runs before every integration. This is Self-Testing Code, a term that emerged from the memory self-tests old computers ran at boot.

The JUnit framework, written by Kent Beck and Erich Gamma, brought this discipline to the Java community in the late 1990s and spawned a generation of similar Xunit tools. These frameworks made it easy to write tests alongside product code, typically showing a green bar for passing tests and a red one for failures. That visual signal gave us the language of "green builds" and "red bars."

A sound test suite should catch a mischievous imp making simple changes — commenting out lines, reversing conditionals — without altering the tests. If any test fails, the build fails. 99.9% green is still red. Perfect testing isn't the bar, though: imperfect tests that run frequently beat perfect tests that never get written.

Testing is the necessary prerequisite to Continuous Integration, and the relationship runs deep — CI originated inside Extreme Programming, where testing was always core. Test Driven Development, writing the test before the code, isn't strictly required but is usually the best way to produce self-testing code. Beyond tests, many environments add further automated checks: linters for style and poor practices, vulnerability scanners for security weaknesses. Evaluate and include these wherever useful.

Commit to Mainline Constantly

Integration is communication. Frequent commits tell the team quickly what's changing. Before pushing to the mainline, update the working copy, resolve conflicts, and build locally — and the build includes the tests. Only then push.

The payoff of frequent commits is early conflict detection. Merge conflicts, where two developers edit the same code differently, are easy for version control to spot — but only once the second developer pulls the latest mainline. Semantic conflicts are far more insidious. A colleague renames a function your new code calls; statically typed languages catch this at compile time, dynamic ones don't. Worse, a colleague changes the body of a function you call in subtle ways. Self-testing code is the only reliable safety net here.

Aim for every developer to commit to the mainline every day; those practiced at CI integrate more often. Each commit should represent a few hours of work. Frequent small chunks track progress and provide a sense of momentum, even if it feels impossible at first to do something meaningful in that window. Mentoring and practice help.

Verify Every Commit With a Build

Even with good discipline, the mainline goes wrong — someone neglects to update before pushing, or a developer's environment differs from the reference. So every commit to the mainline must trigger a full build in a clean integration environment. A Continuous Integration Service — Jenkins, GitHub Actions, Circle CI, and the like — monitors the mainline, checks out the head on each commit, and runs the complete build. Only a green build completes the integration. And because every push is verified, a failure points directly at the latest commit.

Restrict the CI service to the mainline branch. Using it to monitor other branches is fine as automation, but it is not Continuous Integration, which demands that all work coexist on a single branch. You can in principle run CI without a service — someone manually checking out the head onto a dedicated machine — but with robust automation freely available, there's little reason to.

Treat a Broken Build as the Top Priority

CI only works when the mainline stays healthy. A failed integration build needs fixing right away; in Kent Beck's words, "nobody has a higher priority task than fixing the build." That doesn't mean the entire team drops everything — usually a couple of people suffice — but it demands conscious prioritization.

Revert the faulty commit rather than patching it in place, if the cause isn't immediately obvious. Reverting takes the mainline back to the last-known good state, letting whoever needs to diagnose the problem do so in a separate environment while everyone else continues. Some teams go further with a Pending Head, also called pre-tested, delayed, or gated commits. Here the CI service parks pushed commits on another branch until the build passes, only then merging to the mainline. This protects the mainline entirely, though an effective team rarely sees a red build anyway — and when it does, the visibility teaches everyone to avoid it next time.

Chisel the Build Down to Minutes

CI exists for rapid feedback, and a slow build kills it. The XP guideline of a ten-minute build is reasonable for most projects; modern teams hit it regularly. Getting there is worth genuine effort, since every minute saved is multiplied by every developer on every commit.

When builds run an hour, the usual culprit is tests that touch external services like databases. The crucial response is a deployment pipeline, also called a build pipeline or staged build: multiple builds run in sequence. The commit build, triggered by each push to mainline, is the fast one — fast enough to be the primary CI cycle. A two-stage pipeline is a common pattern. Stage one compiles and runs fast unit tests with test doubles standing in for databases and external services, staying inside the ten-minute guideline at the cost of missing interaction bugs. Stage two runs slower end-to-end suites against real infrastructure, possibly taking hours. When that secondary build fails, the team fixes it rapidly but without halting everything. Crucially, every later-stage failure should produce a new test for the commit build, so the fast build gets stronger and the bug stays caught.

Cloud environments make parallel builds practical — spin up a fleet of servers for the build and the speed multiplies, provided tests can run independently. It's also worth automating dependency updates. Most software relies on third-party components, and new versions break things. Check for changes in dependencies and integrate them at least daily, treating them like another team member. This extends to running contract tests against dependent systems. A red signal here doesn't stop the line the way a mainline failure does, but it demands prompt attention.

Integrate Work Before It's Visible

If you integrate as soon as you have forward progress, then unfinished features naturally live on the mainline. This latent code needs careful handling — not because it's unfinished in quality, but because it must not run in production until ready.

The cleanest approach is a Keystone Interface: make the interface that exposes the new feature the very last thing added. Until then, tests can exercise all the underlying code, and the final interface should be small enough to add in a short episode. For assessing production impact before release, Dark Launching runs changes in production without making them visible to users.

Feature Flags cover cases where a keystone won't work. The flag gates execution of latent code, configured in the environment — enabled for testing, disabled in production. Flags also enable A/B testing and canary releases. Once a feature is fully out, remove the flag logic promptly so it doesn't clutter the codebase. Branch By Abstraction is another approach for large infrastructure changes: introduce an internal interface that routes between old and new logic, gradually shifting execution paths over time.

Reversibility is essential. Parallel Change, also called expand-contract, breaks a change into steps you can walk back. Renaming a database field becomes: create the new field, write to both, copy data, read from the new, then drop the old. Each step is reversible, which would be impossible with a single all-at-once change. Keep changes small, and keep them easy to undo.

Test in a Clone of Production

Every difference between test and production environments is a risk that what works in one fails in the other. Match everything: database software and versions, operating system version, libraries — even ones the system doesn't use — IP addresses, ports, hardware. Virtualization makes this practical today, with identical containers running in production, test, and on developers' machines. The cost of this fidelity is small next to debugging an environment mismatch. For software that targets multiple environments, the pipeline should test all of them in parallel. And if production runs on dodgy wifi, as smartphones do, make the test environment bad in the same way.

Make the State Visible

CI is communication, so everyone needs to see the state of the system. CI service dashboards show the status of every build, often broadcasting into Slack and alerting inside IDEs. Notify on successes as well as failures; the regular green signal builds rhythm and confidence, and the occasional "well done" doesn't hurt. Physical displays work well for co-located teams: a large screen with a simplified dashboard, using the familiar red/green of the build. Teams have gotten playful over the years — red and green lava lamps that bubble if the build isn't fixed, a dancing rabbit. Beyond current status, history matters. One team used a wall calendar where QA placed a green or red sticker each day based on whether they received a stable build; the creeping green across the year tracked their improvement until the calendar's purpose was fulfilled and it disappeared.

Automate Deployment as Well

CI moves executables through several environments multiple times a day, so deployment automation isn't optional. Scripts should deploy the product into any environment with ease. Modern tooling goes further still — scripts build the environment itself from a bare-bones base, right through installing and running the product, all automatically. With feature flags in place, test environments can enable every flag to exercise imminent features together.

A natural side effect is that deploying to production becomes just as easy. Teams often ship to production several times daily with these same scripts, and even slower cadences benefit from reduced errors. Automated rollback is the essential companion; the ability to quickly return to the last-known good state removes much of the anxiety that slows deployments. Blue Green Deployment supports both fast releases and fast rollbacks by shifting traffic between versions. Canary releases, which expose a new version to a subset of users first, likewise become practical. Mobile apps present the special case where deployment means getting builds onto test devices before approval gatekeepers get involved.

Make version information discoverable everywhere. An about screen with a build id tied back to version control, logs that say which version is running, and an API endpoint reporting version info all make outages diagnosable and rollbacks precise.

Three Approaches to Integration

There is no single universal way to handle integration. In practice, the approaches teams take fall roughly into three categories, though the boundaries between them are fuzzy. The oldest model, Pre-Release Integration, treats integration as a distinct phase, which fits naturally with a Waterfall Process. Work is split into units, each handled by an individual or small team, with minimal interaction between units. Each unit is built and tested in isolation — the original meaning of “unit test.” Once all units are ready, they are combined into the final product, followed by an integration testing phase and ultimately a release.

In this model, integration frequency is tied to release frequency, often months or years apart for major versions. Urgent bug fixes are typically handled through a separate process so they can ship without disrupting the regular integration schedule.

More recently, Feature Branches have become a common choice. Features are still assigned to individuals or teams, much like units in the older approach, but developers merge their work into the mainline as soon as that feature is complete rather than waiting for all units to finish. Some teams release to production after each merge, while others hold back a few features to batch into a single release.

Teams using feature branches generally pull from mainline regularly, but this amounts to semi-integration. Two developers working on separate features may both pull from mainline daily without seeing each other's changes until one completes and merges their feature. Only then, on the next pull, does the other developer integrate that new mainline code into their working copy. Each push to mainline triggers another round of semi-integration for every other developer, but full integration never actually happens until a developer pushes their own branch — at which point it merely causes yet another round of semi-integrations.

Even if two developers pull the exact same changes from mainline, they have only integrated with that shared code, not with each other's branches. This is the key distinction from Continuous Integration, where everyone pushes changes to the mainline daily and pulls everyone else's work into their own. That creates far more integration sessions, but each one is much smaller — combining a few hours of work is considerably easier than reconciling several days' worth of divergent changes.

The payoff from shrinking the integration window

Most debate about integration styles is really debate about integration frequency. Pre-Release Integration and Feature Branching can both run at different cadences, and you can raise or lower that cadence without switching styles. A team doing Feature Branching with features that each take less than a day to build is effectively doing Continuous Integration. What sets CI apart is that high frequency is the defining property, not a side effect of feature size or release schedule. It makes frequent integration the explicit target, and the habits that sustain it become part of daily work.

Teams that cannot adopt full CI will still see most of the benefits below by integrating more often within their existing style. Shrinking features from two months to two weeks is a real improvement. CI’s advantage is that it bakes high-frequency integration in as the default, making the practice sustainable.

Delivery risk falls as the unknown shrinks

Complex integrations are notoriously hard to estimate. A merge can be painful in git yet work out fine; another can merge cleanly while a subtle conflict takes days to surface. The longer the gap between integrations, the more code must be reconciled, and the harder the effort becomes to predict. That unpredictability is what makes pre-release integration a nightmare: it happens late, when time is scarce and pressure is already high. Teams get stuck in integration hell, where fixing one conflict exposes two more.

Any increase in integration frequency reduces that risk, because there is simply less integration left to do before a release. Feature Branching helps by moving integration work onto individual streams, so a feature can reach mainline as soon as it is done—provided nothing else has landed in the meantime. When other pushes do arrive, the developer on an isolated branch has little visibility into what they contain or how hard they will be to integrate. Priority management helps here: blocking lower-priority pushes can protect a critical feature from integration delays.

Continuous Integration removes this risk almost entirely. Integrations are so small they usually pass without comment; an awkward one takes a few minutes to resolve, and the worst case—a conflict that forces a restart—costs less than a day’s work. Problems surface while the team still has time to deal with them, and the team gets practice resolving conflicts regularly. Even without frequent production releases, CI shows everyone the true state of the product. No hidden integration effort sits between the current code and a release candidate.

Integration time is non-linear—and so is the waste

Hard data is scarce, but anecdotal evidence strongly suggests integration effort does not scale linearly with code volume. Doubling the code to integrate is more likely to quadruple the time. Integration is about connections, and connections grow faster than the things they link. Teams on feature branches feel this waste personally: hours spent rebasing on a large mainline change, days waiting for review while another big change lands, or pausing new work to debug a problem in integration tests for a feature finished weeks ago.

Under CI, integration becomes a non-event. Pull mainline, run the build, push. If there is a conflict, the change is small and fresh, so it is easy to resolve. The regular rhythm makes the workflow practiced and encourages automation. There is a trap here, though: a traumatic integration can convince a team to integrate less often, which only guarantees worse ones later.

What high-frequency integration really does is expose conflicting decisions early. The source-control system becomes a communication channel, surfacing disagreements between developers while they are still cheap to reconcile.

Fewer bugs, not because CI finds them

Continuous Integration does not eliminate bugs, but it makes them dramatically easier to find and remove. That benefit comes mostly from self-testing code, which CI effectively demands: without decent tests, a healthy mainline is impossible. CI institutionalizes a regular testing regimen, so inadequate test coverage becomes obvious quickly and can be corrected. When a semantic conflict does produce a bug, only a small amount of code is in play, so the fault is easy to isolate. Frequent integration also pairs well with Diff Debugging—even a bug noticed weeks later can be traced back to a narrow change.

Bugs are cumulative. Each one makes the others harder to find, because failures often result from multiple interacting faults, and because morale saps the energy needed to hunt them. Self-testing code reinforced by CI has another exponential effect here, steadily reducing the total burden of defects.

This contradicts a common intuition: that high reliability demands slow releases. The DORA research program led by Nicole Forsgren found the opposite. Elite teams deployed to production more rapidly and more frequently, and had dramatically lower failure rates. The same research found higher performance among teams with three or fewer active branches, merging to mainline at least daily, and no code freezes or integration phases.

Refactoring stays affordable

Codebases deteriorate over time. Early decisions stop making sense after months of new learning, but reworking them means intrusive changes deep in existing code—the kind that create long, risky merges. Most teams have a memory of the change that was right for the future but cost days of breaking everyone else’s work, so nobody wants to restructure, even when the current structure slows everyone down.

Refactoring counters that decay with small, behavior-preserving transformations that rarely introduce bugs and can be done quickly on a foundation of self-testing code. The blocker is integration. A two-week refactoring session that greatly improves the code produces painful merges, because everyone else has kept working against the old structure. Frequent integration removes that barrier: when someone makes intrusive changes to a core library, everyone else adjusts only a few hours of work. If directions clash, the conflict surfaces immediately and can be resolved with a conversation.

This is the deepest counter-intuitive claim in software development: teams that spend real effort keeping their codebase healthy deliver features faster and cheaper. Time invested in tests and refactoring pays off in delivery speed, and Continuous Integration is what makes that investment work in a team setting.

Releases become a business call, not an engineering project

When a stakeholder sees a new feature and asks how long until it can go live, the answer depends entirely on integration state. On an unintegrated branch with weak release automation, the answer might be weeks or months. With Continuous Integration and a Release-Ready Mainline, the feature already sits on mainline in a deployable state. The decision to release the latest version becomes a purely business decision, executed in minutes by an automated pipeline. Customers gain control over when features ship, which encourages them to collaborate more closely with the development team.

That removes one of the biggest barriers to frequent deployment. Rapid, frequent releases get new capabilities to users sooner, generate faster feedback, and pull customers into the development cycle. The barriers between customers and developers are the biggest barriers to successful software, and CI is one of the most effective tools for breaking them down.

When Continuous Integration Is Not the Right Call

A list of benefits always deserves a skeptical look, but Continuous Integration is one of the rare practices where the downside is minimal for a team that is both committed and skillful. The cost of sporadic integration is so high that almost any group benefits from integrating more often. The point of diminishing returns sits at hours, not days—precisely the territory Continuous Integration targets. The combination of self-testing code, Continuous Integration, and refactoring is especially powerful. Thoughtworks has relied on this approach for two decades, and the core method is proven; the only open question is how to do it even better.

That said, Continuous Integration is not for everyone. The two adjectives above—committed and skillful—point to the situations where it won't fit.

A committed team works full-time on a product. The classic counter-example is an open-source project run by one or two maintainers with many contributors. Even the maintainers only spend a few hours per week on the code, they don't know the contributors well, and they have little visibility into when contributions arrive or what standards they follow. That environment naturally produced feature-branch workflows and pull-requests. Continuous Integration isn't plausible there, although pushing integration frequency higher can still help.

Full-time commercial software teams are the natural home for Continuous Integration, but most projects sit somewhere in the middle. Judgment is required to pick an integration policy that matches the team's actual commitment.

The second constraint is skill. Attempting Continuous Integration without a strong test suite leaves no mechanism for screening out bugs. Without automation, integration takes too long and disrupts development flow. Without discipline around pushing only green builds to mainline, the mainline ends up broken and blocks everyone. Anyone considering Continuous Integration must account for these abilities. Introducing it without self-testing code will fail—and worse, it will give an inaccurate impression of what the practice looks like when done well.

The skill bar is not especially high, though. Rock-star developers are not required, and they are often a barrier, since people who see themselves that way tend to lack discipline. The technical practices are learnable; the real challenges are finding a good teacher and forming the habits that make the discipline stick. Once a team gets the rhythm, the flow usually feels comfortable, smooth, and fast.

Introducing Continuous Integration

The path into Continuous Integration depends heavily on where you start. No one can know what code you're working on, what skills your team has, or what the organizational context looks like. What follows are common signposts to help you find your own route.

Be clear about why you're introducing the practice. The list of benefits covers the usual reasons, but their importance varies by context. Some benefits are easier to appreciate than others. Reducing integration waste addresses a frustrating problem and progress is immediately felt. Enabling refactoring to cut system cruft and improve productivity is harder to see—the effect takes time and there is no counter-factual to compare against. Yet that is probably the most valuable benefit of all.

The practice list above indicates the skills a team needs to make Continuous Integration work. Some of these pay off even before you reach high integration frequency. Self-testing code adds stability to a system even with infrequent commits.

A useful target is to double the integration frequency. If feature branches typically run for ten days, figure out how to reduce that to five. That might involve better build and test automation, as well as creative thinking about splitting large tasks into smaller, independently integrated ones. If you use pre-integration reviews, include explicit steps to check test coverage and encourage smaller commits.

For a new project, start with Continuous Integration from day one. Watch build times and take action as soon as you drop below the ten-minute rule. Acting early makes the necessary restructuring manageable, before the code base grows large enough to become a serious pain.

Most importantly, get help. Find someone who has done Continuous Integration before; introducing a technique you've never seen in practice is hard. That support may cost money, but without it you'll pay in lost time and productivity. (Full disclosure: Thoughtworks does consultancy in this area—we have made most of the mistakes there are to make.)

Frequently Asked Questions, Answered

Where did Continuous Integration come from?

Kent Beck developed Continuous Integration as a core practice of Extreme Programming during the 1990s. Back then, teams typically integrated code only before a release, with cycles often spanning years. Beck defined the practice, refined it on his own projects, and mapped out its relationship with other supporting practices.

Microsoft had earned a reputation for daily builds, usually run overnight, but those builds lacked the automated testing and the discipline of immediately fixing defects that are central to Continuous Integration.

Grady Booch is sometimes credited with coining the term, but he used it only once, in passing, within his object-oriented design book. He didn't treat it as a defined practice — the phrase didn't even make it into the index.

What's the difference between Continuous Integration and Trunk-Based Development?

As CI services gained popularity, many teams used them to run regular builds on feature branches. That is not Continuous Integration at all, but it led plenty of people to believe they were practicing CI when they were doing something fundamentally different.

Trunk-Based Development emerged as a term to counter that semantic diffusion. In general, it means the same thing as Continuous Integration, and its advantage is that it doesn't suffer the same confusion with "running Jenkins on our feature branches." Attempts to draw a sharper distinction between the two are neither consistent nor compelling.

I avoid the term, partly because renaming isn't a good cure for semantic diffusion and partly because it erases the work of Beck and others who originally championed the practice. That said, there is excellent material on Continuous Integration published under the Trunk-Based Development banner, notably Paul Hammant's extensive writings.

Can a CI service run on feature branches remain useful?

The short answer: yes, automated builds on feature branches are worthwhile — but that's semi-integration, not Continuous Integration. The core principle is that everyone commits to the mainline every day.

The confusion stems from calling these tools "Continuous Integration Services." A better name would be "Continuous Build Services." A CI service is a useful aid to Continuous Integration, but the tool shouldn't be confused with the practice.

Can a team combine Continuous Integration with Feature Branching?

In most cases, the two are mutually exclusive. Teams that think they're doing both are typically running a CI service on feature branches, which, as explained above, isn't Continuous Integration.

There is one rare exception: branches so small they can be completed within a day. In that scenario, most people would simply call the workflow Continuous Integration anyway.

A related point: personal work on a separate branch is perfectly fine, as long as it's merged back to main when integrated. The question is whether integration happens continuously, not how individual developers organize their workspaces.

How does Continuous Integration differ from Continuous Delivery?

Early descriptions of CI focused on the integration cycle within the dev team's environment and said little about the journey to production. That doesn't mean the path was ignored — practices like "Automate Deployment" and "Test in a Clone of the Production Environment" show the release path was on people's minds.

In some projects there wasn't much left after mainline integration. Beck once showed me a Smalltalk system in Switzerland that deployed to production automatically every day. But at Thoughtworks in the early 2000s, production deployments frequently involved much more complex steps. That gap gave rise to the notion of an activity beyond CI — Continuous Delivery.

The aim of Continuous Delivery is to keep the product always in a releasable state, so that shipping to production is purely a business decision. Today, many draw the line with CI ending at the mainline integration and CD covering the rest of the deployment pipeline to production. Some treat CD as encompassing CI, others describe them as close partners under the label CI/CD, and a few argue CD is nothing more than a synonym for CI.

Where does Continuous Deployment fit?

These three practices form a progression. CI ensures everyone integrates at least daily to the mainline. CD ensures the product is releasable whenever anyone chooses. Continuous Deployment goes further: the product is automatically released to production every time it passes the full set of automated tests in the deployment pipeline.

With Continuous Deployment, every commit pushed to mainline as part of CI is automatically deployed once all pipeline verifications are green. Continuous Delivery merely guarantees that deployment is possible, making it a prerequisite for Continuous Deployment.

What about pull requests and code reviews?

Pull requests, introduced by GitHub, are now ubiquitous. They add process around pushing to mainline, typically requiring a pre-integration code review and an approval from another developer. This model grew out of feature branching in open-source projects, where maintainers need to vet contributions from loosely connected outsiders.

Pre-integration review creates real friction for CI. Instead of an automated, minutes-long process, review demands finding someone, scheduling their time, and waiting for feedback. Even where the flow stays within minutes, it easily stretches to hours or days — breaking the cadence that makes CI work.

Teams that practice CI reframe how review happens. Pair programming provides continuous, real-time review as code is being written, with a far faster feedback loop. The Ship / Show / Ask model encourages reserving blocking reviews for situations that genuinely require them, since post-integration review interferes less with integration frequency. Many find Refinement Code Review invaluable for maintaining a healthy code base — and it works best when CI fosters an environment friendly to refactoring.

Pre-integration review emerged for an open-source context where contributions arrive unexpectedly from unrelated developers. Practices that suit that setting need rethinking for a tight-knit, full-time team.

How do you handle databases?

Databases present a special challenge as integration frequency rises. Schema definitions and test data load scripts can live in version control with ease, but that doesn't address data outside version control — most notably production data. When the schema changes, how do existing data get handled?

Traditional pre-release integration treated data migration as a heavyweight effort, sometimes justifying dedicated teams. High-frequency integration would seem, at first glance, to make migration work untenable.

Experience shows a shift in perspective dissolves the problem. On early Thoughtworks CI projects, we solved it through Evolutionary Database Design, developed by my colleague Pramod Sadalage. The method defines both schema and data through a series of small migration scripts, each easy to reason about and test. Migrations compose naturally — hundreds can run in sequence to accomplish substantial schema changes while migrating data along the way. Stored in version control in sync with application data access code, migrations make it possible to build any version of the software with the matching schema and data. Those same migrations run on test data and production databases alike.

Closing Thoughts

Most software work means modifying existing code. How quickly and cheaply new features can be added depends greatly on the health of that code. A crufty code base is harder and more expensive to change, so a team must be able to refactor regularly — adjusting structure to match shifting needs and lessons learned from working on the product.

Continuous Integration is essential to this evolutionary design ecosystem. Supported by self-testing code, it underpins refactoring. These technical practices, born together in Extreme Programming, let a team steadily enhance a product and respond to new opportunities.

Where to Go Next

Continuous Integration sits inside a much larger decision space. Choosing when to branch and when to merge into a shared mainline is the constant driver; CI is the practice that makes frequent integration safe. For more on that context, my article on Patterns for Managing Source Code Branches covers branching strategy in detail.

Paul Duvall’s book on Continuous Integration, which won a Jolt award, remains the most thorough treatment of the topic. For the broader Continuous Delivery pipeline, Jez Humble and Dave Farley’s book is the standard reference. My earlier original article from 2000 documents the first Thoughtworks project to use CI at scale.

Much of the current writing on this subject uses the term “Trunk-Based Development.” Paul Hammant’s website is a practical resource, and Clare Sudbery has written an informative report available through O’Reilly.

Credits and History

The foundation for this work comes from Kent Beck and the C3 project team, where I first saw Continuous Integration combined with meaningful unit tests. Matt Foemmel, Dave Rice, and the Atlas team later demonstrated CI’s value on an established, larger-scale project. Paul Julius, Jason Yip, Owen Rodgers, Mike Roberts, and other contributors built CruiseControl, the first CI service, which helped popularize the practice even though such a service is not strictly required.

Michael Lihs suggested revisions in late 2023 that prompted a major overhaul. Birgitta Böckeler, Camilla Crispim, Casey Lee, Chris Ford, Clare Sudbery, Evan Bottcher, Jez Humble, Kent Beck, Kief Morris, Mike Roberts, Paul Hammant, Pete Hodgson, Rafael Detoni, Rouan Wilsenach, and Trisha Gee reviewed and commented on the revision. Nearly every project I have visited at Thoughtworks has contributed practical insights into continuous integration practice.

Significant revisions: the original version was published 10 September 2000; a full rewrite followed on 01 May 2006; another rewrite began 18 October 2023 and the revised version was published 18 January 2024.