
At some point, every programmer discovers a painful truth:
Writing code is not the same thing as having software.
Code is the source material. Software is what happens after the code is compiled, bundled, linked, packaged, configured, copied into the correct place, given the right permissions, supplied with the correct libraries, and launched in an environment that has not decided to become a haunted vending machine.
Welcome to CS202 Episode 5.
This is build systems and dependency management.
Or, in plain English: how your project goes from “a bunch of files that look promising” to “a thing another human can actually run without summoning you from dinner.”
And yes, this matters.
It matters because modern software is rarely one lonely file bravely doing its best. Real software is usually a crowd scene. Source files. Libraries. Frameworks. Assets. Configuration. Generated code. Environment variables. Database migrations. Container images. Test runners. Package registries. CI pipelines. A small army of tools with names that sound like indie bands.
The build system is the part that tries to make all of that repeatable.
The dependency manager is the part that tries to bring everyone’s friends without accidentally inviting a raccoon with administrator privileges.
So let’s talk about how software becomes real.
Not spiritually real.
Runnable real.
The kind of real where your teammate can clone the project, run the documented command, and not immediately question every career decision that led them here.
If you have been following the path so far, CS202 began with Version Control and Change Management, because teams need history. Then APIs and Program Boundaries, because software needs agreements. Then Error Handling and Defensive Programming, because reality keeps entering through the side door. Then Refactoring and Code Smell, because code ages like fruit unless somebody pays attention.
Now we reach the part where the code must become an artifact.
That word matters.
An artifact is the output of the build process: an executable, a library, a bundle, a package, a container image, a deployable archive, or whatever final object your system needs in order to run.
Source code is the recipe.
The artifact is the cake.
The build system is the kitchen, the oven, the timer, the checklist, and sometimes the person yelling, “Who moved the flour?”
Why build systems exist
A build system exists because “just run it” stops working surprisingly early.
In a tiny beginner project, running software can feel simple:
python main.py
Or:
node app.js
Or:
gcc hello.c -o hello
./hello
Lovely.
Adorable, even.
But then the project grows.
Now there are multiple source files. Some need to be compiled before others. Some code is generated. Some assets need to be copied. Some tests must run. Some dependencies need to be downloaded. Some files should not be rebuilt unless they changed. Some platforms need different flags. Some secrets should not be baked into the final package unless you enjoy explaining things in meetings.
Suddenly “just run it” becomes:
clean the old output
install packages
generate types
compile source
bundle assets
copy config
run tests
produce release build
package artifact
verify artifact
deploy artifact
apologize to future maintainers
A build system turns that pile of steps into a process.
A good build system answers four basic questions:
What needs to be built?
What does it depend on?
What steps produce it?
Can we skip any work safely?
That last question is more important than it looks.
If your project has 10 files, rebuilding everything is fine.
If your project has 10,000 files, rebuilding everything every time is how your laptop becomes a space heater with opinions.
Build systems are not just about automation. They are about correct automation.
They encode the relationships between parts of the system so the computer can do the boring, fragile, order-sensitive work without relying on human memory, which is a noble but deeply inconsistent technology.
The old idea hiding underneath the modern tools
Build systems feel modern because today we talk about npm, Maven, Gradle, CMake, Bazel, Cargo, Make, Vite, Webpack, esbuild, Docker, GitHub Actions, and enough configuration formats to make a file extension nervous.
But the underlying idea is old and beautifully simple:
If one thing depends on another thing, build the dependency first.
That is it.
That is the little engine inside the whole machinery.
If main.o depends on main.c, and app depends on main.o and utils.o, then the build system needs to know that order.
A tiny C example:
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c utils.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
clean:
rm -f app *.o
This is a simplified Makefile.
Do not panic if you have never used Make. The point is not the syntax. The point is the relationship.
app depends on main.o and utils.o.
main.o depends on main.c and utils.h.
utils.o depends on utils.c and utils.h.
If utils.c changes, the build system should rebuild utils.o, then rebuild app.
If main.c has not changed, maybe main.o can be left alone.
That is the beginning of incremental builds: only rebuilding what is necessary.
This is one of those ideas that feels tiny until you have a large codebase and every full rebuild costs 20 minutes. At that point, incremental builds are not an optimization. They are mercy.
Build systems are dependency graphs wearing work boots
Underneath most build systems is a graph.
Not a chart with vibes. A real dependency graph.
A node is something involved in the build:
- a source file
- an object file
- a generated file
- a library
- a test target
- a bundle
- a container image
- a final executable
An edge means “this thing depends on that thing.”
If app depends on core, and core depends on config, then config must be available before core, and core must be available before app.
In CS101, Algorithmic Thinking taught us that order matters. In CS101, Data Structures taught us that how we organize information changes what we can do efficiently. Build systems use both ideas constantly.
The build graph tells the build tool what order is legal.
If the graph is clean, the system can decide:
Build this first.
Then this.
Then these two can happen in parallel.
Then package the final result.
This is why modern build tools care so much about dependencies being declared accurately. If the graph is wrong, the build system may do work in the wrong order, skip something it should have rebuilt, or rebuild the entire universe because it no longer trusts anybody.
And honestly, relatable.
The great enemy: hidden dependencies
A declared dependency is one the build system knows about.
A hidden dependency is one that exists but is not written down.
Hidden dependencies are where builds become cursed.
Imagine your code generation step reads a file called schema.json, but your build script does not declare that dependency.
The build system thinks:
“Nothing changed. We are fine.”
But schema.json did change.
Now the generated code is stale, the application behaves strangely, and someone says, “But it works on my machine,” which is usually the opening line of a tragedy.
A dependency is hidden when the build needs something but the build tool does not know it needs it.
Common examples:
A script reads a config file without declaring it.
A test relies on a local database.
A build step uses an environment variable.
A compiler uses files discovered through include paths.
A frontend build grabs assets from a folder nobody listed.
A developer has a global package installed that the project forgot to specify.
This is why reproducibility matters.
A reproducible build means that given the same source, the same dependencies, and the same configuration, the build produces the same result.
That sounds obvious until you meet software.
Modern systems are full of invisible influences: operating system versions, compiler versions, package versions, time zones, filesystem case sensitivity, CPU architecture, local tools, environment variables, network access, and one mysterious file installed two years ago by a tutorial you no longer remember.
A good build process tries to drag those invisible influences into the light.
Not because engineers are control freaks.
Well, not only because of that.
Because if the build cannot be repeated, the software cannot be trusted.

Dependency management: the part where your code makes friends
Dependency management is the practice of declaring, downloading, versioning, and updating external code your project relies on.
Almost every modern project uses dependencies.
A web app may use a UI library.
A backend may use a database driver.
A mobile app may use an HTTP client.
A C++ project may use a logging library.
A Python tool may use a package for parsing dates, because date handling is where optimism goes to be lightly injured.
Dependencies are powerful because they let us avoid rewriting everything from scratch.
That is good.
You do not want every team writing its own encryption library, JSON parser, image decoder, test framework, build runner, HTTP stack, and date formatter.
That way lies madness, security bugs, and a function called parseDateReallyThisTime.
But dependencies also create obligations.
When you add a dependency, you are not just adding code.
You are adding:
- a version
- a license
- a maintenance history
- a security surface
- possible transitive dependencies
- build requirements
- compatibility constraints
- update responsibilities
That phrase “transitive dependencies” is where the floorboards creak.
A direct dependency is one you explicitly ask for.
A transitive dependency is a dependency of your dependency.
You install library A.
Library A needs library B.
Library B needs library C.
Library C needs three other things, one of which was last updated when people still thought QR codes were a temporary fad.
Now your project depends on all of them.
This is why installing one library can sometimes summon six hundred of its friends.
It is not magic.
It is a graph.
Again.
Software engineering keeps returning to graphs the way toddlers return to the one cabinet they are absolutely not supposed to open.
Version constraints: promises with tiny teeth
Dependency managers usually let you describe which versions of a package are acceptable.
For example, a JavaScript project might have something like this:
{
"dependencies": {
"some-library": "^2.4.1"
}
}
Different ecosystems interpret version ranges in their own ways, but the concept is the same:
“I need this package, but not just any version. I need a version compatible with what my code expects.”
This matters because dependencies change.
A package may fix bugs.
A package may add features.
A package may introduce breaking changes.
A package may remove behavior your code accidentally relied on while everyone looked away politely.
Semantic versioning tries to bring order to this by using versions like:
MAJOR.MINOR.PATCH
In the ideal version of the universe:
PATCH updates fix bugs without changing behavior.
MINOR updates add features without breaking existing code.
MAJOR updates may break compatibility.
In the actual version of the universe:
That is the intention, and intentions are nice. They wear clean shirts.
Version numbers are promises, not force fields.
A dependency manager helps, but it cannot guarantee that every package author follows the rules perfectly. This is why mature teams use lockfiles.
A lockfile records the exact versions that were resolved and installed.
Examples include:
package-lock.jsonyarn.lockpnpm-lock.yamlCargo.lockpoetry.lockGemfile.lock
The dependency file says what you allow.
The lockfile says what you actually got.
That distinction matters a lot.
Without a lockfile, two developers may install slightly different dependency versions on different days.
With a lockfile, the team has a shared snapshot of the dependency graph.
It is not glamorous.
It is extremely useful.
Many engineering practices are like that. No fireworks. Just fewer Tuesday disasters.

Compilation pipelines: the journey from source to artifact
Build systems often coordinate a pipeline.
A pipeline is a sequence of transformations.
For compiled languages, a simplified pipeline might look like this:
source code
↓
preprocessing
↓
compilation
↓
object files
↓
linking
↓
executable
In CS201, How a Computer Actually Runs a Program introduced the idea that source code does not levitate into execution. Something must translate it into a form the machine can run. Later in CS201, Operating Systems in Plain English showed why running programs live inside an environment managed by the operating system, not in a cozy fantasy cottage outside reality.
Build systems sit right in that practical gap.
They coordinate the steps.
They pass flags.
They decide where output goes.
They make sure generated files exist before compilers need them.
They package results for the environment where they will run.
For a frontend project, the pipeline may look more like:
TypeScript
↓
JavaScript
↓
bundling
↓
minification
↓
asset hashing
↓
deployable static files
For a backend service, it may include:
compile code
run tests
build container image
scan dependencies
publish image
deploy to staging
For a mobile app, it may include signing, platform-specific packaging, assets, metadata, and enough configuration screens to make a strong person whisper.
The details change.
The principle does not.
A build turns inputs into outputs through a repeatable process.
A concrete example: the tiny project that got real
Imagine a small TypeScript app.
At first, it has one file:
src/main.ts
You run it directly using a development tool.
Then the project grows:
src/main.ts
src/user.ts
src/api.ts
src/config.ts
public/logo.png
package.json
tsconfig.json
Now you need to:
Install dependencies.
Compile TypeScript.
Bundle JavaScript.
Copy static assets.
Run tests.
Produce a dist folder.
A minimal package script might look like this:
{
"scripts": {
"clean": "rimraf dist",
"build": "tsc",
"test": "vitest run",
"start": "node dist/main.js"
}
}
Now a developer can run:
npm install
npm run build
npm test
npm start
That is already better than tribal knowledge.
But notice the hidden assumptions.
Which Node version?
Which npm version?
Are dependency versions locked?
Does npm install produce the same packages on every machine?
Does the build require environment variables?
Does start require a database?
Does the test suite depend on files outside the repository?
Can it run on a clean machine?
The build script is not the end of the conversation.
It is the start of making the conversation explicit.
That is the deeper lesson.
Build engineering is mostly about removing the phrase “Oh, you also need to…” from your project.
A more realistic example: the service that ships to production
Now imagine a real backend service.
It has:
- application code
- unit tests
- integration tests
- database migrations
- generated API clients
- a Dockerfile
- environment-specific configuration
- a CI pipeline
- security scanning
- deployment manifests
The build and release process might do this:
1. Check out source code
2. Install dependencies from lockfile
3. Generate code from API schema
4. Compile application
5. Run unit tests
6. Run integration tests
7. Build container image
8. Tag image with commit hash
9. Scan image and dependencies
10. Push image to registry
11. Deploy to staging
12. Run smoke tests
13. Promote to production
This is not just “building.”
This is turning source control history into a deployable, traceable, testable artifact.
That traceability matters.
If production is running version abc123, the team should be able to answer:
What source code produced it?
What dependencies were used?
What tests passed?
What build tool version ran?
What environment produced the artifact?
Can we rebuild it?
Can we roll it back?
Can we explain it without pointing at Gary because Gary is on vacation and should be left alone?
That is why build systems are part of engineering discipline, not just convenience.
The goal is not only to make software run.
The goal is to make software run again, somewhere else, by someone else, under pressure, without mythology.

Common misconception: dependency managers solve dependency problems
They help.
They do not solve the entire problem.
A dependency manager can download packages, resolve versions, and produce a dependency tree.
It cannot automatically decide whether a dependency is wise.
It cannot guarantee the package is maintained.
It cannot guarantee the license fits your project.
It cannot guarantee the package is secure.
It cannot guarantee an update will not break behavior.
It cannot guarantee your architecture is healthy just because the install command succeeded.
This is an important CS202 lesson:
Tools automate decisions.
They do not eliminate responsibility.
A package manager is like a very efficient assistant. It can bring you what you asked for. It may even bring the exact version you asked for. But it will not always stop you from asking for trouble with a confident voice.
That part is still engineering.
Common failure modes
Build and dependency problems tend to show up in recognizable shapes.
The clean machine failure
The project works on one developer’s laptop but fails on a fresh machine.
Usually this means something was installed globally, configured locally, or accidentally omitted from the setup instructions.
The cure is to test setup from scratch.
A container can help.
A CI runner can help.
A new teammate with honest confusion can also help, though do not treat them as unpaid monitoring equipment. They have feelings and probably snacks.
The stale artifact problem
The source changed, but the output did not rebuild.
This usually means the build system did not know about a dependency.
The graph was wrong.
The computer did exactly what you told it, which remains one of computing’s most annoying habits.
The dependency drift problem
Different machines install different versions.
The usual cure is a lockfile, pinned tool versions, and consistent install commands.
In serious environments, teams may use private registries, dependency mirrors, or build containers to reduce surprise.
The circular dependency problem
Module A needs module B.
Module B needs module C.
Module C needs module A.
Now the build order is a little snake eating its own calendar.
Circular dependencies are often a design smell. They mean parts of the system are too entangled. Sometimes the fix is to extract a shared interface, split a module, invert a dependency, or rethink the boundary.
This connects directly back to CS102’s Modular Design and Abstraction, where we learned that modules should cooperate without becoming emotionally fused.
The “works in dev, fails in production” problem
Development uses one environment.
Production uses another.
Different OS, different architecture, different compiler flags, different dependency versions, different environment variables, different file paths, different permissions, different phase of the moon. Fine, probably not the moon. But after enough incidents, you start checking.
This is why teams try to make environments more consistent.
Containers help.
CI helps.
Infrastructure-as-code helps.
Clear configuration helps.
But the concept is older than the tools: reduce the number of differences between where you build, where you test, and where you run.
The tradeoffs: speed, correctness, simplicity, control
Build systems are full of tradeoffs.
A very simple build script is easy to understand but may be slow or fragile.
A sophisticated build system can be fast and reproducible but harder to learn.
A flexible dependency range can make updates easy but increase surprise.
A pinned dependency can improve repeatability but delay important updates.
A monorepo build system can coordinate huge codebases but may require serious tooling discipline.
A small project may not need Bazel-level machinery.
A large project may suffer badly without it.
This is where engineering maturity means choosing the right amount of machinery.
Not too little.
Not too much.
Just enough structure that the project can grow without every build becoming a campfire story told in a trembling voice.
Build complexity should be earned.
But repeatability should start early.
Even a tiny project benefits from a clear setup command, a build command, a test command, and a note about required tool versions.
Future You will appreciate it.
Future You is already tired.
Be kind.
Build systems in the age of AI coding tools
Modern AI tools can generate code quickly.
That is useful.
It also makes build discipline more important, not less.
Because if code appears faster, integration problems appear faster too.
An AI assistant may suggest a library.
But is it already in the project?
Does it match the project’s ecosystem?
Does it introduce licensing concerns?
Does it conflict with existing versions?
Does it work with the current runtime?
Does it require a build step nobody added?
Does it pass tests in CI?
The build is where generated suggestions meet reality.
This is one reason professional software teams care so much about automated builds and tests. They are not trying to slow creativity down. They are creating a controlled landing strip for change.
Without a reliable build, AI-generated code can make a mess at machine speed.
With a reliable build, AI can become part of a healthier workflow: propose, integrate, build, test, review, ship.
The build system becomes the bouncer at the nightclub of reality.
It does not care how charming the code sounded in chat.
It checks the list.
What students should actually remember
You do not need to memorize every build tool.
Tools change.
Concepts last longer.
Remember these:
A build turns source inputs into runnable or deployable outputs.
A build system makes that process repeatable.
Dependencies are relationships your project relies on.
Dependency managers help fetch and resolve external code.
Transitive dependencies are dependencies of dependencies.
Lockfiles record exact resolved versions.
Incremental builds save time by rebuilding only what changed.
Reproducible builds reduce “works on my machine” disasters.
Hidden dependencies are dangerous because the build system cannot manage what it cannot see.
Build graphs explain why order matters.
A broken build is not just an inconvenience. It is a communication failure between source code, tools, environment, and humans.
And perhaps most importantly:
A project that cannot be built reliably cannot be changed safely.
That is why this topic belongs in CS202.
At this level, students are no longer just learning how to write code.
They are learning how people build software other people can survive.
The build is where that survival begins.
Closing thought
A good build system is not glamorous.
Nobody throws a parade because npm ci worked, the tests ran, the binary linked, and the artifact deployed exactly as expected.
But that quiet success is the point.
Good engineering often looks boring from the outside because the chaos has been handled before it reached the audience.
The build system is one of the places where that happens.
It takes your source code, your dependencies, your tools, your configuration, your tests, your packaging rules, and your deployment expectations, and says:
“Fine. Let us make this real. But we are writing it down this time.”
And honestly?
That is a beautiful little act of civilization.
Follow along for the next CS202 stop, where we move into debugging at scale, also known as “when the bug is not in one line, but in the relationship between eight ideas and one service that is lying with confidence.”
If this helped, follow me, share it with someone whose build folder looks emotionally complex, and leave a comment with the build tool that has either saved your life or stolen part of your afternoon.
A graceful decorative garden scene inspired by elegant turn-of-the-century poster art, featuring a serene figure in flowing robes standing among tall lilies, curling vines, and radiant circular halos of soft gold light. Use sinuous organic lines, flat ornamental shapes, delicate floral patterns, muted peach, sage green, ivory, pale lavender, and warm amber tones. The composition should feel symmetrical but alive, with sweeping hair-like curves, elongated forms, refined outlines, and a dreamy theatrical mood. Keep the scene family-friendly, polished, luminous, and free of readable text, logos, brands, modern objects, or recognizable people.

Begin with a sudden bloom of golden ornamental lines spiraling outward like living ink, then reveal the garden figure as lilies open in rhythmic pulses around the frame. Let vines curl across the screen in elegant loops, halos brighten behind the figure, and soft petals drift forward in layered parallax. Add quick, graceful transitions where floral patterns unfold like paper fans, robes ripple in a light breeze, and the whole scene glows with warm amber flashes timed to the beat. Keep the motion refined, catchy, dreamlike, family-friendly, and free of readable text, logos, brands, or recognizable people.
Song Recommendations:
Sweet Dreams, TN — The Last Shadow Puppets
Moonlight — Kali Uchis