---
title: 'Modules and Dependencies'
source: 'https://academia.sh/en/courses/go-fundamentals/modules-and-dependencies'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes, in a dependency graph of six requests, how many are written in the source and how many are derived'
    - 'Applies the minimal version selection rule by running it on a model, without making a real network call'
    - 'Shows, by measuring it, why the selected version does not have to be the same as the newest version available'
---

# Modules and Dependencies

A dependency's version is most often never written in the source at all; of six requests, three are written in the main module's own source, three are derived from a dependency's own dependency. The module file's rule for filling in an unwritten version — minimal version selection — is modeled and run within the source; no network call is made. The limiting measurement shows that the selected version is not the newest version in a module's catalog, only the largest of the requests actually written.

The previous lesson showed that a package's boundary stops at the package, not at the file. This
lesson carries the same question one level up: when a package depends on another package, where
in the source is **which version** of that dependency meant? Most often, nowhere at all. A
module file writes the version of a handful of modules it directly depends on; the version of
those modules' own dependencies is never written at all, it is derived from the modules' own
files. This lesson's question is the same as the course's general question: when the version is
not written, what does the module file put in its place?

A dependency graph has a problem its version resolution has to solve: if the same module is
requested through more than one path in the graph, **which** version that module should be
compiled with is not written on any single line of the source. When two separate dependencies
request the same third module with different minimum versions, the main module is forced to
choose between the two — but this choice does not appear in the main module's source at all,
because the main module never mentions that third module directly. This situation comes up often
in dependency graphs by another name: two separate paths arrive at the same third point, and two
different requests collide there. The collision is not an error — both requests say "at least
this version," both draw only a lower bound, neither draws an upper bound. The job of the
selection is to merge these two lower bounds into **a single version** and make sure the merged
version satisfies both requests. The measurement below builds this situation in its smallest
form and reads the result by actually running the selection algorithm.

## Six Requests, Two Sources

The measurement below never connects to a real module registry at all; the dependency graph is
built by hand within the source, and **it is written here explicitly that it is modeled**. There
are four modules: `libraryA`, `libraryB`, `libraryC`, `libraryD`. The main module directly
requests `libraryA`, `libraryB`, and `libraryD` — all three versions are written in the source.
`libraryA` requests one version of `libraryC`; `libraryB` requests another version of the same
`libraryC`. `libraryC`, for its own part, requests one version of `libraryD`. These three
requests are never written in the main module's source at all — they are **derived**, during the
main module's compilation, from the dependency modules' own files.

```go
// main.go — minimal version selection, the dependency graph is modeled in the source (no network call)
package main

import "fmt"

// Version carries a three-part version number; this lesson does not connect to a real
// module registry, the whole graph below is constructed by hand.
type Version struct {
	Major, Minor, Patch int
}

func (v Version) text() string {
	return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}

// greaterThan reports whether v is a newer version than other.
func (v Version) greaterThan(other Version) bool {
	if v.Major != other.Major {
		return v.Major > other.Major
	}
	if v.Minor != other.Minor {
		return v.Minor > other.Minor
	}
	return v.Patch > other.Patch
}

// Request carries the lowest version a module's dependency requests. If Written is
// true, the request sits directly in the main module's own source; if not, it was
// derived from another dependency's own module file.
type Request struct {
	Module    string
	From      string
	Requested Version
	Written   bool
}

func main() {
	requests := []Request{
		{Module: "libraryA", From: "main module", Requested: Version{1, 2, 0}, Written: true},
		{Module: "libraryB", From: "main module", Requested: Version{1, 1, 0}, Written: true},
		{Module: "libraryD", From: "main module", Requested: Version{2, 1, 0}, Written: true},
		{Module: "libraryC", From: "libraryA", Requested: Version{1, 4, 0}, Written: false},
		{Module: "libraryC", From: "libraryB", Requested: Version{1, 6, 0}, Written: false},
		{Module: "libraryD", From: "libraryC", Requested: Version{2, 0, 0}, Written: false},
	}

	fmt.Println("-- six requests, is the source written or derived --")
	writtenCount, derivedCount := 0, 0
	for _, req := range requests {
		source := "derived"
		if req.Written {
			source = "written"
			writtenCount++
		} else {
			derivedCount++
		}
		fmt.Printf("%-12s <- %-12s %-9s requested=%s\n", req.Module, req.From, source, req.Requested.text())
	}
	fmt.Printf("total: %d written, %d derived\n", writtenCount, derivedCount)

	fmt.Println()
	fmt.Println("-- selected version: the greatest of all requests for each module --")
	orderedModules := []string{"libraryA", "libraryB", "libraryC", "libraryD"}
	selected := map[string]Version{}
	for _, req := range requests {
		current, ok := selected[req.Module]
		if !ok || req.Requested.greaterThan(current) {
			selected[req.Module] = req.Requested
		}
	}
	for _, name := range orderedModules {
		fmt.Printf("%-12s selected=%s\n", name, selected[name].text())
	}

	fmt.Println()
	fmt.Println("-- limiting measurement: selection takes not the newest, but the greatest written --")
	catalogNewestC := Version{1, 9, 0}
	fmt.Printf("newest version in the catalog for libraryC: %s\n", catalogNewestC.text())
	fmt.Printf("selected version: %s\n", selected["libraryC"].text())
	fmt.Printf("is the selected version the same as the catalog's newest: %v\n", selected["libraryC"] == catalogNewestC)
}
```

```
-- six requests, is the source written or derived --
libraryA     <- main module  written   requested=1.2.0
libraryB     <- main module  written   requested=1.1.0
libraryD     <- main module  written   requested=2.1.0
libraryC     <- libraryA     derived   requested=1.4.0
libraryC     <- libraryB     derived   requested=1.6.0
libraryD     <- libraryC     derived   requested=2.0.0
total: 3 written, 3 derived

-- selected version: the greatest of all requests for each module --
libraryA     selected=1.2.0
libraryB     selected=1.1.0
libraryC     selected=1.6.0
libraryD     selected=2.1.0

-- limiting measurement: selection takes not the newest, but the greatest written --
newest version in the catalog for libraryC: 1.9.0
selected version: 1.6.0
is the selected version the same as the catalog's newest: false
```

**PT8.** Three of the six requests are written in the source, three are derived; which three are
written depends on whether that module is a **direct** dependency of the main module. **PT9.**
`libraryC` has two separate requests — one from `libraryA`, one from `libraryB` — and both come
into being without ever appearing in the source at all, purely from these two modules' own
files.

## Minimal Version Selection

**PT10.** A module's selected version is the greatest of all the requests written for that
module; when both `1.4.0` and `1.6.0` are requested for `libraryC`, the selected version is
`1.6.0`, and when both `2.0.0` and `2.1.0` are requested for `libraryD`, the selected version is
`2.1.0`. The rule's name is **minimal version selection**, but the selected number is the
greatest of the requests, not the smallest — this seemingly contradictory naming resolves once
you look at what the rule actually guarantees. The rule says: let the selected version be **the
lowest version that violates no request**. A dependency requesting `1.4.0` also works with
`1.6.0` (versions are assumed backward compatible), but a dependency requesting `1.6.0` gives no
guarantee at all that it will work with `1.4.0`. This is why the lowest version satisfying **all**
requests is the greatest of the requests — the word "minimal" does not point to a single request,
it points to the single point that satisfies every request at once.

This is a computation in which backward compatibility is taken as an assumption; the rule itself
never checks this, it only accepts it. The `greaterThan` comparison above compares three numbers
in order and never tests anywhere at all whether two versions are really compatible —
compatibility is accepted as a promise attached to the version number itself, and it is never
separately verified. The selection algorithm gives the correct result in a world where this
promise is kept; when the promise is not kept (when the new version does not offer the same
interface as the old one), the algorithm still selects the same number, but whether the selected
version actually works is a separate question — the answer to that question is found not in the
module file, but in whether the source itself compiles.

This computation also runs, for `libraryD`, through a two-layer chain: `libraryD` receives one
request directly from the main module (`2.1.0`, written) and receives another request indirectly
through `libraryC` (`2.0.0`, derived). Selection makes no distinction at all between the two —
requests are compared not by where they come from but by which number they carry, and the
greater one (`2.1.0`) wins. Whether a request is written or derived is not an **input** to the
selection algorithm at all; it is only a separate observation, classifying the request's source,
that this lesson measures.

The importance of this distinction grows as the number of requests grows. In the measurement
above there are four modules and six requests; in a real dependency graph this number can be
measured in the hundreds, and the same module being requested from dozens of different paths is
an ordinary situation. The selection rule does not change at this scale either: for every
module, the greatest of the versions requested by all paths leading to that module is taken. The
algorithm itself is insensitive to the depth of the graph — whether the request reaching
`libraryD` comes from two steps away through `libraryC`, or directly from the main module, the
comparison consists of nothing more than placing the same two numbers side by side and picking
the larger one. The complexity sits in the size of the graph, not in the rule itself.

## The Limiting Measurement: Not the Newest, the Greatest Written

The run above also carries one more catalog value: the "newest version available" for `libraryC`
is given as `1.9.0`, but the selected version is `1.6.0`. **PT11.** Selection never asks at all
about the newest version a module has available; it only looks at requests actually written (or
derived), and takes the greatest of those. This is the most misunderstood side of minimal
version selection: the rule does not **keep a dependency up to date**, it only satisfies whatever
has been requested somewhere in the source (directly or indirectly). `libraryC` may have a
`1.9.0` version, but as long as no request asks for it, it never enters the selected version at
all.

That this is a deliberate choice becomes visible when you look at what it actually guarantees:
it is wanted that a dependency graph's result be independent of who compiles that graph and when.
If selection were "always take the newest," the same source text could compile with a different
set of dependencies at different times — even though the source itself never changed at all.
Minimal version selection instead looks only at what is written in the source (even if
indirectly); the same set of requests selects the same set of versions no matter when or where it
is run. The cost is explicit: to benefit from a dependency's new version, **a line requesting**
that version has to be written somewhere; no request rises on its own.

This has one more consequence: two separate developers, running the same dependency graph at the
same time, with no knowledge of each other at all, still arrive at the same set of versions —
because selection looks at no outside state at all (the catalog's current contents, which
versions are considered "newest"), only at what is written in the source. Upgrading a dependency
therefore requires a separate, deliberate step: either the directly requested version is changed
in the source, or an intermediate dependency's own request somewhere in the chain is raised.
Either way, the upgrade happens by **writing** a new number somewhere; until it is written,
selection always returns to the same result, because the computation itself is deterministic.

## Written, Derived, Selected

In a module file's real source, these three concepts fall on three separate lines. The versions
the main module directly requests sit explicitly written in the module file's own declaration
block. Modules carrying only a derived version also fall into the same block, but these lines
are marked "indirect" — they show modules never `import`ed anywhere in the source, only an input
to the selection computation. The selected version emerges from the union of these two sets,
through the computation above; because the module file carries these three separately, why a
module ends at that version — its own request or another dependency's request — remains
traceable backward from the source.

In a real module file, this declaration block can be read line by line: every line carries a
module name and the version selected for that module, and a short note is added at the end of a
line for a module not directly requested but only derived. This note itself is not a version
piece of information, it is a piece of **origin** information — it says that module was never
`import`ed at all in the main module's source, it only entered the selection computation through
another module. The `Written` field in the measurement above is exactly the counterpart of this
note: for `libraryA`, `libraryB`, `libraryD` the `Written` field is `true` (these are `import`ed
in the main module's own source), and `false` for `libraryC`'s two requests (neither is ever
mentioned at all in the main module's source). The declaration block itself is written **after**
the selection computation — first every request is gathered, minimal version selection is run,
then only the result (the selected version) falls into the block. The intermediate steps — which
request came from which module — never appear at all in the module file itself; this information
exists only for a moment while the selection computation is running, exactly as it sits in the
`requests` slice above.

These three distinctions (written, derived, selected) have a practical consequence: someone
reading a module file can tell apart, with two different questions, why a dependency stands
there. The answer to "did I request this module?" sits in the direct/indirect distinction. The
answer to "which version did we get?" sits on the selected version line — but this second
question never comes out of **any single** line of the source alone, because the selected version
is the result of comparing more than one request. The module file is therefore less a
declaration list and more the **final state of a computation**: every line carries the winner of
a comparison that does not appear in the source.

Reading where a dependency's version comes from off a declaration line (**Modules, Tooling and
the Ecosystem**) and determining which version a package is published under (**Python Projects:
Packaging and Testing**) were already built in other languages' own mechanisms; this lesson does
not repeat that model, it only measures Go's own selection rule — taking the greatest of the
requests.

## Summary

- Requests in a dependency graph come from two sources: those written in the main module's own
  source, and those derived from a dependency's own module file.
- Minimal version selection takes the **greatest** of all requests written for a module; it
  carries the name "minimal" because it is the lowest common point that violates no request.
- Whether a request is written or derived does not affect the selection computation; only which
  number is carried is compared.
- The limiting measurement: the selected version is not the newest version in a module's
  catalog, only the greatest of the ones actually requested — no request rises on its own.
- This measurement was run without making a real network call at all, by modeling the
  dependency graph within the source.

## Next Step

This lesson measured who selects a module's version: the choice is determined neither by a
person nor by a separate tool, it is determined by the requests themselves — whether written in
the source or derived from a dependency, the computation applies the same rule to both. A
module's version therefore sits outside the source, as an output of the selection algorithm. But
for the **form** of the source inside a module and the **convention** it follows, the answer to
the same question sits in a different place entirely: not a person or a rulebook applies them,
but Go's own command family. The next lesson calls these commands from within the program and
measures how an unwritten formatting rule changes the source, and which tool catches a pattern
the compiler does not reject.
