Artifact Repositories
An artifact repository (also called a binary repository manager or package registry) is the storage system that sits between a build and everything that consumes its output: it holds the compiled
jar, the Pythonwheel, the npm tarball, the Go module zip, the Debian.deb, the RPM, and the container image, and it serves them back — versioned, immutable, and access-controlled — to developers, CI jobs, and production deployers. The two dominant self-hosted products are JFrog Artifactory and Sonatype Nexus Repository, and both organize storage around the same three-way abstraction: repositories you publish to (local/hosted), repositories that cache an upstream (remote/proxy), and repositories that aggregate several behind one URL (virtual/group) (JFrog remote repositories; Sonatype Nexus). The repository is where the CI/CD load-bearing rule build once, promote the artifact becomes physical: the exact bytes a build produced are stored once, addressed by a unique coordinate, and promoted onward unchanged. This note covers the machinery of that storage — repository types, pull-through caching, retention, and immutability; the discipline of never re-tagging over a published version lives in Immutable Artifact Versioning, and the container-specific registry API lives in Container Image Registries.
Mental Model — Three Repository Types
The single most important idea is that “repository” is not one thing. Every serious repository manager distinguishes three kinds, and the whole design of a build’s dependency resolution flows from getting them straight (JFrog local/remote/virtual):
flowchart TD DEV["Developer / CI job<br/>points its client at ONE URL"] DEV --> V["<b>Virtual repo</b> (JFrog)<br/><b>Group repo</b> (Nexus)<br/>single logical endpoint<br/>aggregates the others"] V -->|"1 · resolve"| L["<b>Local repo</b> (JFrog)<br/><b>Hosted repo</b> (Nexus)<br/>YOUR published artifacts<br/>the ones you build"] V -->|"2 · resolve"| RC["<b>Remote-cache</b><br/>previously proxied<br/>upstream artifacts"] V -->|"3 · resolve"| R["<b>Remote repo</b> (JFrog)<br/><b>Proxy repo</b> (Nexus)<br/>caching proxy of an<br/>UPSTREAM registry"] R -.->|"on cache miss,<br/>fetch on demand"| UP["Upstream<br/>Maven Central · npmjs.org<br/>PyPI · proxy.golang.org<br/>Docker Hub · deb/rpm mirrors"] style L fill:#1a5,color:#fff style R fill:#a51,color:#fff style V fill:#25a,color:#fff
What it shows and the insight to take: a client (a developer’s mvn, npm, pip, go, apt, or docker command) is configured with one URL — the virtual (JFrog) or group (Nexus) repository. That aggregator resolves a request by walking its member repositories in configured order: local first (your own builds — fastest and authoritative), then the remote-cache (things already pulled from upstream), then the remote proxy itself, which reaches out to the real upstream only on a miss (JFrog virtual repositories). The insight: developers never point at Maven Central or npmjs.org directly. They point at your aggregator, which gives you a single choke point for caching, access control, and — critically for supply chain — a record of exactly which third-party bytes entered your builds.
The three types map cleanly across the two products, with only the names differing:
| Role | JFrog Artifactory | Sonatype Nexus | What it does |
|---|---|---|---|
| Publish target | Local repository | Hosted repository | Stores artifacts your organization produces (Nexus) |
| Upstream cache | Remote repository | Proxy repository | Caches an upstream registry, fetching on demand (JFrog) |
| Aggregator | Virtual repository | Group repository | Presents many repos under one URL with a resolution order (JFrog) |
Mechanical Walk-through — Pull-Through Caching
The remote/proxy repository is the piece most worth understanding mechanically, because it is a proxy, not a mirror. JFrog states the distinction precisely: “Artifacts are not pre-fetched to a remote repository cache. They are only fetched (pulled) and stored (cached) on demand when requested by a client” (JFrog remote repositories). Nothing is copied ahead of time; the cache fills lazily as builds actually request things. This lazy pull-through is where the term pull-through cache comes from.
sequenceDiagram participant CI as CI job (mvn/npm/pip) participant V as Virtual repo participant RC as Remote-cache participant R as Remote repo (proxy) participant UP as Upstream (Maven Central) Note over CI,UP: First request — cache MISS CI->>V: GET junit/junit/4.13.2/junit-4.13.2.jar V->>RC: check local + remote-cache RC-->>V: not present V->>R: resolve via remote proxy R->>UP: GET junit-4.13.2.jar UP-->>R: 200 + bytes R->>RC: store in <repo>-cache R-->>CI: 200 + bytes Note over CI,UP: Second request — cache HIT CI->>V: GET same jar V->>RC: check remote-cache RC-->>CI: 200 (served from cache, no upstream hit)
What it shows and the insight to take: the first request for a third-party artifact pays a round-trip to the upstream and populates the cache; every subsequent request is served locally at LAN speed and never touches the internet again. The cached copy lives in an internal repository whose name is the remote repository’s name with -cache appended — you can address it directly at http://<host>/artifactory/<remote-name>-cache/<path> to read only what is already cached and skip the upstream freshness check (JFrog remote repositories). (Because -cache is reserved for this, Artifactory forbids remote repository names ending in -cache.) The payoff is threefold: speed (LAN vs WAN), resilience (a build still works when the upstream is down, if the artifact is cached), and governance (every foreign byte is recorded and scannable in one place).
Freshness: how the cache decides when to re-check upstream
A pure pull-through would go stale, so the proxy has to decide when a cached answer is still trustworthy. Immutable release artifacts (a specific junit-4.13.2.jar) never change, so they can be cached forever. But metadata — the index that says “what versions of junit exist” — does change as new versions are published upstream. Artifactory governs this with cache-period settings (JFrog remote repositories):
- Metadata Retrieval Cache Period — how long metadata files are trusted before Artifactory re-checks upstream for newer versions. Default is 2 hours (7200 s) for most types, 6 hours for Docker/OCI/Helm.
- Missed Retrieval Cache Period — how long a
404 Not Foundis remembered, so a request for a nonexistent artifact does not hammer the upstream on every build. Default 1800 s;0disables negative caching. - Assumed Offline Period — after a connection failure, how long to wait before probing the upstream again (default 300 s), plus a per-repo Offline flag and a system-wide Global Offline Mode that turns every remote into a cache-only repository for air-gapped operation.
The distinction between caching an immutable file forever and re-validating mutable metadata on a timer is the heart of why proxies work: the thing you cache aggressively (release binaries) is exactly the thing that is guaranteed never to change — the immutability property that Immutable Artifact Versioning is built on.
Per-Format Repositories — One Tool, Many Package Types
A repository manager is format-aware: it does not just store bytes, it speaks each ecosystem’s native protocol and generates each ecosystem’s index metadata. Nexus and Artifactory both support the full spread — Maven, npm, PyPI, Docker/OCI, Go, apt (Debian), yum (RPM), Helm, NuGet, Cargo, Conan, and more (Sonatype Nexus). The point of a single tool is that one aggregator URL serves a polyglot monorepo or a fleet of services in different languages.
| Ecosystem | Artifact | Client | Upstream proxied | Index metadata the repo generates |
|---|---|---|---|---|
| Java/JVM | .jar, .pom | mvn, gradle | Maven Central | maven-metadata.xml |
| Python | .whl, .tar.gz | pip, uv | PyPI | PEP 503 “simple” index HTML |
| Node | .tgz tarball | npm, bun, pnpm | registry.npmjs.org | package.json version doc |
| Go | module .zip + .info + .mod | go | proxy.golang.org | @v/list, @latest (GOPROXY protocol) |
| Containers | OCI image (manifest + layers) | docker, podman | Docker Hub, GHCR | OCI distribution /v2/ API |
| Debian | .deb | apt | deb.debian.org mirrors | Packages, Release files |
| RHEL | .rpm | dnf, yum | mirror.centos.org etc. | repodata/repomd.xml |
Each row is a different wire protocol, but the repository-type model (local/remote/virtual) is identical across all of them. For Go specifically, the repository is a GOPROXY — the go command downloads immutable module snapshots through it, and the proxy caches them and serves them unchanged (Go modules reference). Container images are the one format important enough to get their own note — see Container Image Registries for the OCI distribution API and digest-addressing that back the Docker row.
Immutability and Retention
An artifact repository is only trustworthy if a coordinate resolves to the same bytes forever. Different ecosystems enforce this with different strictness, and the repository manager typically inherits and enforces the ecosystem’s rule:
- Maven draws a hard line between release and SNAPSHOT versions. A release version (any version without the
-SNAPSHOTsuffix) is “unchanging” — once1.0is deployed to a release repository it cannot be overwritten, so a consumer of1.0always gets identical code. ASNAPSHOTis explicitly the mutable development version and can be re-deployed, with the repository storing timestamped copies (Maven getting started). Repository managers reflect this by configuring a repo to accept either releases or snapshots, rejecting a re-deploy of an existing release. - npm treats “registry data” as immutable: once
bob@1.1.0has been published, “no other package can ever be published with that name at that version. This is true even if that package is unpublished” (npm unpublish policy). - PyPI forbids filename reuse entirely — a deleted file’s name can never be uploaded again; the API returns “Filename has been previously used.” The stated rationale is that “a given distribution for a given release for a given project will always resolve to the same file, and cannot be surreptitiously changed one day by the project’s maintainer or a malicious party (it can only be removed)” (PyPI help).
- Go ties immutability to a public checksum database: a module version is “an immutable snapshot,” and the hashes recorded in
go.sum(and cross-checked againstsum.golang.org) mean any attempt to serve different bytes for the same version is detected as a security error (Go modules reference).
Retention is the counterweight to immutability: because artifacts are never modified, they only accumulate, and storage grows without bound unless something prunes it. Repository managers offer retention / cleanup policies to bound the pile — typically by age (delete artifacts older than N days) or count (keep only the most recent N versions of an artifact). Artifactory’s Docker cleanup, for example, limits the number of tags per image and runs a garbage collector that “automatically removes unreferenced Docker layers — those no longer used by any image after tags are deleted or expire,” done “transparently and automatically, behind the scenes, without any downtime” (JFrog blog). Container storage in particular benefits from this because images share layers (see Container Image Layers and Copy-on-Write): deleting a tag does not free space until every image referencing a given layer blob is gone, so a mark-and-sweep garbage collector — not a simple delete — is required.
flowchart LR subgraph Retention decision A["Artifact in repo"] --> Q{"Immutable<br/>release?"} Q -->|"Yes — never mutate"| K{"Retention<br/>policy"} Q -->|"SNAPSHOT / dev"| M["May be overwritten;<br/>old timestamps prunable"] K -->|"age > maxDays"| DEL["Mark for deletion"] K -->|"version count > maxCount"| DEL K -->|"within policy"| KEEP["Keep"] DEL --> GC["Layer/blob GC:<br/>free only when<br/>zero references remain"] end
Caption: retention operates on top of immutability, not against it — you never edit a release, you only decide whether to keep or delete it wholesale, and shared-blob garbage collection reclaims space only when the last reference disappears.
Failure Modes and Common Misunderstandings
“A remote repository is a mirror.” No — it is a lazy proxy. Nothing is copied until a client requests it. If your upstream disappears and you never cached artifact X, artifact X is gone from your builds too. Teams that assume “we proxy Maven Central, so we’re safe from an outage” discover on the outage day that only the already-requested artifacts survived. If you need a guaranteed-complete offline copy, you must pre-warm the cache or use a true mirroring mode, not a proxy.
Trusting the upstream directly. Pointing pip or npm straight at PyPI/npmjs from CI means every build reaches out to the public internet, you have no record of what entered your builds, and you inherit the upstream’s availability. A proxy repository is a supply-chain control point: it can be scanned, access-controlled, and audited. This is the mechanism side of what DevSecOps and Supply Chain Security MOC governs on the policy side.
Resolution-order surprises. In a virtual/group repo, a name that exists both locally and upstream resolves to whichever member is ordered first — normally local wins (JFrog virtual repositories). This is deliberate and useful (your internal com.acme:widget shadows any public one) but it is also the dependency-confusion attack surface: if an attacker publishes a public package with your internal name and your resolution accidentally prefers the remote, a malicious version can slip in. The defense is scoping internal names and ordering local ahead of remote — a resolution-order decision made in the repository manager.
Re-deploying over a published version. Some repository managers allow overwriting a release by default unless configured otherwise. Doing so silently breaks the “same coordinate → same bytes” guarantee everyone downstream relies on. Why this is a genuine supply-chain footgun, and how immutable-tag settings prevent it, is the whole subject of Immutable Artifact Versioning.
Forgetting negative-cache windows. If a build asks for a version that does not exist yet upstream, the 404 may be cached (Missed Retrieval Cache Period). Publish that version upstream and your build can still see the stale 404 until the window expires — a confusing “it’s published but CI can’t find it” symptom with a one-line explanation.
Alternatives and When to Choose Them
| Option | Model | Choose when |
|---|---|---|
| JFrog Artifactory | Self-hosted/SaaS, universal (all formats), local/remote/virtual | You want one tool for every language + deep JFrog Platform (Xray scanning, distribution); enterprise scale |
| Sonatype Nexus Repository | Self-hosted/SaaS, universal, hosted/proxy/group | Same universal need, strong open-source heritage (Nexus OSS is free); Maven-centric shops |
| Cloud-native registries (GCP Artifact Registry, AWS CodeArtifact, Azure Artifacts, GitHub Packages) | Managed, tightly integrated with the cloud’s IAM/CI | You are all-in on one cloud and want zero ops; IAM-native access control |
Language-native only (a bare Go GOPROXY, a private PyPI via devpi, Verdaccio for npm) | Single-format, lightweight | You only need one ecosystem and do not want a heavyweight universal manager |
| Container-only (Harbor, GHCR, Docker Hub) | OCI registry, images (+ increasingly any OCI artifact) | Your artifacts are overwhelmingly container images — see Container Image Registries |
The honest trade-off: universal managers (Artifactory/Nexus) win when you are polyglot and want a single caching/governance choke point; cloud-native registries win when you value zero-ops and cloud-IAM integration over cross-cloud portability; single-format tools win for small, focused setups where a universal manager is overkill.
Production Notes
In practice the highest-value configuration is boring but decisive: make every developer and every CI runner resolve through your aggregator, never the public upstream. That one rule gives you a cache (fast, resilient builds), an audit trail (what third-party code entered which build), and a scanning hook (block known-vulnerable dependencies at the proxy). It is also the precondition for reproducibility — a build that pulls from a controlled, cached proxy is far more likely to produce the same result next month than one pulling live from the churning public internet.
The second high-value practice is separating local repositories by promotion stage or trust level — e.g. a libs-release-local that only ever receives promoted, blessed artifacts, distinct from a libs-staging-local for candidates. This makes Artifact Promotion Across Environments a repository move (or a property/tag change) rather than a rebuild, embodying Build Once Promote the Artifact. Container-image promotion is the same idea expressed with digests instead of file paths.
Finally, treat retention as a first-class policy, not an afterthought. Immutable artifacts accumulate forever; without age/count cleanup and layer garbage collection your storage bill and backup windows grow without bound. The tension to manage: prune aggressively enough to control cost, but never prune something a running production deployment still references — which is exactly why the garbage collector frees a shared layer only when its reference count hits zero (JFrog blog).
Uncertain
Verify: the exact default cache-period values (2 h metadata / 6 h Docker-OCI-Helm / 1800 s missed / 300 s offline) and the reserved
-cachesuffix behavior are quoted from the JFrog remote-repositories doc as fetched 2026-07-25; JFrog occasionally revises defaults between Artifactory releases. Reason: single-vendor doc, version not pinned on the page. To resolve: confirm against the Artifactory version in use (System → repository configuration UI shows the live defaults). Nexus’s per-format proxy cache TTLs differ and were not separately fetched.#uncertain
See Also
- Immutable Artifact Versioning — the discipline of never re-tagging over a published version; semver vs digest-pinning (the why behind repository immutability rules)
- Container Image Registries — the OCI registry API, push/pull by digest, tags vs digests, image-layer garbage collection (the container-specific instance of this note)
- OCI Image and Distribution Specifications — the content-addressed manifest/blob model that container repos implement
- Artifact Promotion Across Environments — promoting the same stored artifact through repositories per stage
- Build Once Promote the Artifact — the pipeline rule that makes the repository the single source of promoted bytes
- Container Image Layers and Copy-on-Write — why layer sharing makes registry garbage collection a mark-and-sweep problem
- The Go Build Cache — a different kind of cache (build outputs), contrasted with the artifact/dependency cache here
- DevSecOps and Supply Chain Security MOC — dependency scanning, dependency-confusion defenses, and provenance that hook into the repository
- Continuous Integration and Delivery MOC — parent MOC (§4 Artifacts and Registries)