Pular para o conteúdo principal
The zero-to-master on-ramp — Git, the command line, a systems language, and the tooling every engineer uses daily.

Foundations & Tooling

The zero-to-master on-ramp — Git, the command line, a systems language, and the tooling every engineer uses daily.

Project Structure, Packaging & Dependency Management

A project’s structure — where files live, how packages depend on each other, what version of what library you actually run — dictates how easily you can ship a one-line fix on a Tuesday. The work happens in the first few decisions of a project’s life and then is paid every single day after.

This topic is the layout and dependency layer that every engineer operates inside of daily.

The Layout Choice

LayoutShapeTrade-off
MonorepoOne repository, many projects (e.g., apps, libraries, services) in subdirectoriesCross-cutting changes are atomic; tooling must scale (Vercel Turborepo, Nx, Bazel)
Polyrepo (multi-repo)One project per repo; cross-repo changes are PR hopsSimple per-repo tooling; cross-cutting work is serial and drift is invisible
HybridMonorepo for tightly-coupled components, separate repos for things that should be standaloneThe default at most mid-sized companies

The choice is by rate of cross-cutting change. If your checkout team and your payments team must ship together weekly, they belong in one repo. If they release independently into stable contracts, polyrepo is fine. The mistaken instinct is “we’ll split when we scale” — premature splitting costs more than premature merging.

Package Managers Per Ecosystem

Each language ecosystem has its own dependency layer with its own conventions. The mechanics rhyme; the commands differ.

EcosystemManager(s)FileLockfile
JavaScriptnpm, pnpm, yarn (Berry)package.jsonpackage-lock.json / pnpm-lock.yaml / yarn.lock
Pythonpip, poetry, uvpyproject.toml (+ requirements.txt)poetry.lock / uv.lock
RustcargoCargo.tomlCargo.lock
Gomodgo.modgo.sum
Java / JVMMaven, Gradlepom.xml / build.gradle(no canonical lockfile; Lockfiles via plugins)
RubybundlerGemfileGemfile.lock

Three behaviors to internalise regardless of ecosystem:

  • Resolve: the manager reads the manifest, walks declared deps, computes a compatible version tree.
  • Fetch: download the resolved tree from a registry.
  • Lock: write the exact resolved versions (including transitive deps) to a lockfile so that a future install reproduces the same tree byte-for-byte.

Lockfiles and Reproducible Installs

A lockfile records the exact resolved version and hash of every package in the install tree — not just what you asked for, but what you got. Two principles:

  1. Commit the lockfile. Without it, “works on my machine” stops at your laptop. The lockfile is how CI and every developer’s laptop produce byte-identical builds.
  2. Lockfiles are an audit artefact. When a CVE drops, you look up the affected versions in your lockfile. Without it, you can only guess what you ran.

For applications, commit the lockfile always. For libraries, conventions vary — npm suggests you commit; pip tooling historically didn’t. Modern advice: lock libraries too, because the lockfile still tells you what was tested.

Semantic Versioning as a Contract

SemVer is a versioning scheme: MAJOR.MINOR.PATCH (1.4.2). The implicit contract:

BumpAllowed changeCaller compatibility
PATCH (1.4.2 → 1.4.3)Bug fixes onlyDrop-in replacement
MINOR (1.4.2 → 1.5.0)Backward-compatible new featuresDrop-in replacement
MAJOR (1.4.2 → 2.0.0)Breaking changesCaller must update

Two declared range operators dominate in lockfiles:

  • ^1.4.2 (caret) — ”≥ 1.4.2 and < 2.0.0”; accepts patches and minor bumps.
  • ~1.4.2 (tilde) — ”≥ 1.4.2 and < 1.5.0”; accepts patches only.

The contract holds only if maintainers follow it. In practice they often do not: they break in minor bumps (most common), or in patch releases (rare but spectacular — the left-pad-event-stream family). The lockfile is your defence: pin to hashes in CI, review all PRs that bump a locked version.

Transitive Dependencies

Most of your installed packages you did not ask for. They are transitive — dependencies of dependencies of dependencies. Two consequences:

  1. You inherit the security posture of your entire dependency tree. A malicious package three levels down runs the same code in your CI as one you wrote. Most supply-chain attacks target transitives (e.g., event-stream → flatmap-stream, the 2018 npm attack).
  2. Dedup matters. If two of your deps ask for different majors of the same library, you get two copies — twice the size, twice the memory, possibly twice the bugs.

Tools that surface the transitive picture: npm ls / pnpm why, pipdeptree, cargo tree, go mod why. Run one at least weekly. Understanding the tree you actually have — not the one you declared — is the dependency-management job.

Practice Trajectory

  1. Run npm ls or its equivalent in your largest project. Identify one transitive package you cannot explain; trace its introducer; decide whether it’s still needed.
  2. Audit a package.json for caret vs tilde ranges. List the libraries that you really want pinned to patches only and switch their range.
  3. Try npm install --package-lock-only against a fresh clone on two machines — one with the lockfile committed, one without. Compare install results.
  4. Sketch the layout decision for a hypothetical startup: payments + checkout + ML inference. Pick monorepo vs polyrepo; justify in one paragraph by rate of cross-cutting change.
  5. Audit your lockfile for duplicated major versions. Pick one duplication; decide whether to unify (and which side of the version split to back).

When It’s the Right Tool

SituationTakeaway
Layout decision for a multi-team projectRate of cross-cutting change drives monorepo vs polyrepo
“Works on my machine” PP failuresThe lockfile is the contract; commit it
A transitive package is suspectAudit the tree, not the manifest; remove the introducer if the suspect is unwanted
A library break sitting on minor versionTighten to tilde on prod-critical deps; semver is hope, not a guarantee
Reviewing a lockfile PR diffLook for major-version jumps anywhere transitive, not only on declared deps