Discovery (proposal)
jenesis.repository.discover is set.
A Maven groupId and a Java module name are reversed domain names: net. belongs to bytebuddy..
This proposal lets whoever owns that domain say, in one small file on its website, where the artifacts and
modules named after it are published. A build that reads the file downloads them from there - a GitHub release,
the author's own server, or a Maven repository of the author's choosing - before it asks any central repository.
The file is a plain
java.
file at a fixed address, so any tool can read it with the JDK alone,
and it names Maven groups and Java modules rather than a build tool. Publishing it costs the author a static
file; reading it costs a build one request per vendor.
Do you suggest not having a central repository?
No. A central repository that keeps every version ever published, immutable and in one place, has real value, and nothing here replaces it. It is also a great way to distribute a small open-source project released now and then: one account and one upload, and every build finds the library, with no hosting for its author to keep alive. That low barrier to entry is worth keeping. But the infrastructure is expensive, and Maven Central - Maven's default repository, and the one most Java builds resolve from - is under constant pressure to pay for itself.
Maven Central is, in effect, a monopoly: one repository, run by one company, that every build looks to and that no build replaces on its own. That is a problem in itself, whatever its operator does today. Its operator, Sonatype, is a private firm, majority-owned since 2019 by the private equity firm Vista Equity Partners. A private firm can change its course quickly, all the more after a change of ownership, and the Java ecosystem has no say in either. An alternative is therefore a contribution merely by existing: the textbook check on a monopoly is not that it behaves well, but that its users could go elsewhere.
Since October 2026, publishing an artifact of a commercial nature to Maven Central requires Sonatype's paid Publisher Pro, and so does publishing beyond monthly quotas on file count, release count and release size, which took effect the same month. Sonatype sets the quotas where the busiest tenth of publishers begin, counts every signature, checksum, POM, sources and javadoc jar as a file, and says the quotas may be adjusted over time. Community projects can ask for an exemption, which Sonatype grants case by case.
Byte Buddy would, for the most part, have stayed within the file and release quotas set today. A release of it now publishes about 120 files and close to 70 MB, though, so a second release in the same month passes the size quota - as happened in about half the months since 2023 in which Byte Buddy released. And nothing guarantees that the quotas will not be lowered: the free Community Edition of Nexus Repository was launched in 2025 with limits of 100,000 components and 200,000 requests, which a later release cut to 40,000 components and 100,000 requests a day.
What quotas do to what is published
The quotas are counted per organisation, across all of its namespaces, and every release must carry sources, javadoc, signatures and checksums. An author who nears them can only publish less: merge small modules into larger ones or drop them, release fixes less often, or stop publishing a second project that shares the organisation. Each of these choices is reasonable for one author. Together they leave less on offer - coarser modules, slower fixes, and side projects that are never published - and that is a cost every user of the ecosystem pays, though no invoice shows it.
A second leg of distribution
The discovery file turns the question around: it lets authors distribute what they build themselves, beside the central repository rather than instead of it, and it frees them from fitting their work to someone else's quota. Some already do: the Shibboleth project does not publish OpenSAML to Maven Central, because Central's terms require an indemnification its developers will not take on personally - the older versions found there were uploaded by others - and Jenkins releases its plugins and libraries from its own repository alone. Today, a build finds such a library only once its user configures that repository; with a discovery file, it is found by its name.
Other ecosystems already distribute this way. Go
asks the domain of a module path where its code lives, through a go-import
tag the domain serves, and puts a proxy and a checksum database
on top as a cache and a ledger, not as a place every module must be uploaded to. Homebrew
formulae download each package from wherever its project hosts it,
checked against a SHA-256, and taps let anyone publish formulae from a repository
of their own.
- Authors decide where they publish what, and how often, including those who do not accept a central repository's terms.
- Ownership follows the domain. Sonatype grants a new groupId to whoever proves, by a DNS record, that they own the domain it reverses to - the very domain whose file a build reads. In 2024, MavenGate showed how lapsed domains could be bought to take over groupIds; Sonatype answered that its checks prevent it and disabled the accounts of expired domains. A discovery file, by contrast, always speaks for whoever owns the domain now. What protects a build is what it recorded: pinned checksums - as Jenesis, Maven's trusted checksums, Gradle's dependency verification and Bazel's pinned Maven installs record them, compared side by side - and declared signatures.
- Authors control their costs. An author can remove an outdated, unmaintained version to save hosting, and publish modules as finely, and new versions as often, as the work calls for, without a subscription.
- Central repositories stay useful. A file can point at Maven Central itself, which is where builds look today anyway. A repository that keeps the full collection can offer it as a service, and companies have even more reason than before to keep copies of what they depend on in their own mirrors.
- The load is shared. Every download served by an author's own hosting is one Maven Central does not serve.
Hosting thereby moves back towards the authors and developers who produce the code, as a second leg of library distribution that changes nothing for those who also publish to a central repository - and that leaves what an author publishes to the author.
Describing Maven artifacts
A domain publishes the file at https:/, a
well-known location, in UTF-8. Its maven
key says where the artifacts of every group below that domain are:
maven=https://github.com/jenesis/jenesis/releases/download/v{version}/{artifactId}-{version}{-classifier}.{type}
maven.suffixes=none
This file at jenesis. says that the group build. is attached to the GitHub releases of Jenesis,
and that those releases hold no snapshots. A dependency on build. then downloads
https:/.
Which file answers
For net., a tool reads the file of bytebuddy. - the two labels a vendor owns - and the
file of agent. only where bytebuddy. publishes none. The first file found speaks for every
name below its domain, so one request answers for all of a vendor's groups, and a key it does not hold is
absent rather than asked of a subdomain. A vendor whose subdomains publish files of their own adds stop=:
those files are then read as well, the most specific one holding a key answers, and the vendor's own entries
stand for the rest.
Roots and templates
A location takes one of two forms:
- A root is a URI without placeholders: a traditional Maven repository such as Maven Central, a Nexus or an
Artifactory, or any web server holding files in the Maven layout. It is read with its checksums, and its
maven-metadata.is merged with that of the usual repositories, so a version range sees the versions of both -xml maven=.https:/ / maven. example. com/ releases/ - A template names each file through placeholders, which suits a flat list of downloads such as the assets
of a GitHub release. Where a template names
{type}, each file is checked against the.,sha512 .orsha256 .beside it, the strongest one present, and a mismatch fails the build.sha1
A Maven template fills in {groupId} (net.), {groupPath} (net/), {artifactId},
{version}, {-classifier} (-sources, or nothing for the plain jar) and {type} (jar, pom, jar.,
jar.). A template without {-classifier} or {type} serves only the plain jar, so a sources jar or a
signature is never answered with the jar itself.
Which versions a key serves
Two keys beside a key restrict the versions it serves, and leave every other version to the usual repositories:
<key>.names the first version, so an author can move downloads to a new place from one release on. Versions are ordered as Maven orders them:since= <version> 1.comes before2. 3-rc. 1 1.,2. 3 1.after it.10. 0 <key>.lists the qualifiers it serves - the part of a version after its first dash, matched by its leading word, ignoring case.suffixes= <suffix>[,<suffix>. . . ] nonenames a version without one; without the key, every version is served.
With maven. and maven., 1. is served, while 1. and everything
before 0. is not. The metadata of a root lists only the versions its key serves.
The newest version
A template cannot list versions, so "the newest release" comes from a link beside it, <key>.. A tool
sends the link a HEAD request, follows no redirect, and reads the version from where it redirects to, matched
against the template:
maven.latest=https://github.com/jenesis/jenesis/releases/latest/download/{artifactId}.pom
GitHub redirects
releases/ to releases/ of the newest release, whatever
<name> is, so the link names the version without downloading anything. The version is then
checked like any other, and the template answers Maven metadata naming it, merged with that of the usual
repositories.
Reading the files with Jenesis
Jenesis reads the files where jenesis. is set, on the command line or in
jenesis.. It asks about every Maven group before any configured repository, reads each domain's file
once per build, and resolves what no file names as it always has:
jenesis.repository.discover=true
A location over plain http is followed only where jenesis. allows it, and under
jenesis. no domain is asked at all.
From modules to Maven artifacts
A module that is published as a Maven artifact is mapped to it by the moduletomaven key, whose value is a
Maven coordinate, <groupId>:<artifactId>[:<extension>[:<classifier>]], without its version:
moduletomaven=build.jenesis:{module}
{module} is the module's name, so this one line maps every module below jenesis. to the artifact of the
same name in the group build.. The artifact then resolves as any Maven dependency does - through the
maven key of the previous chapter and the usual Maven repositories - and a request without a version takes the
newest release its Maven metadata names. A domain that publishes both keys needs no entry in a module index.
Where artifact names follow another pattern, {-suffix} follows it. It is the part of the module's name below
the file's domain, its labels joined by dashes after a leading one: nothing for net., -agent for
net.. One line maps every Byte Buddy module:
moduletomaven=net.bytebuddy:byte-buddy{-suffix}
A module whose suffix names no artifact, such as net., finds nothing and resolves as if the
file did not name it. A coordinate without placeholders belongs to one module only - the one whose own domain
publishes the file - so an artifact that follows no pattern is named at its module's own domain.
A build that resolves modules through Maven, reading their POMs, asks moduletomaven first. In Jenesis that is
the modular_ layout, the one a module-info. gets by default.
Pure module repositories
A module that is not published to Maven, or a build that resolves modules on the module path alone, uses the
module key. Its value is a location, in the same two forms:
- A template names each module file.
{module},{-suffix},{version},{-classifier}and{type}are filled in, and files are checked against their checksums as for Maven. - A root is a Jenesis module service, such as
https:/, asked as a configured module repository is./ repo. jenesis. build/
module=https://github.com/jenesis/jenesis/releases/download/v{version}/{module}-{version}{-classifier}.{type}
module.latest=https://github.com/jenesis/jenesis/releases/latest/download/{module}.jar
module.suffixes=none
A build that resolves a module from its jar and the requires of its module-info asks module first - in
Jenesis, the modular layout - and so downloads build. with no POM and no Maven at all.
module. names the newest version for a requires that no pin names. A Jenesis module service may serve
as the latest link too, https:/: it names the version in a
Jenesis-ModuleVersion header, which is read before the redirect.
A key that does not answer leaves the request to the other, so a domain that publishes both module and
moduletomaven serves either kind of build.
Publishing everything a build may ask for
One release can serve every kind of build at once: the module path, a module resolved through Maven, and a Maven
dependency, each with its sources, javadoc, signatures and checksums. Jenesis does so itself. Its release
stages a Maven repository, and JReleaser attaches the jar, the POM, the sources jar and
the javadoc jar to the GitHub release. JReleaser signs
every file it attaches, and checksum.
has it upload a . beside each, as in this excerpt of Jenesis's own
jreleaser.:
signing:
active: ALWAYS
pgp:
armored: true
mode: MEMORY
release:
github:
owner: jenesis
name: jenesis
tagName: 'v'
checksum:
individual: true
files:
artifacts:
- path: 'target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis-.jar'
- path: 'target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis-.pom'
- path: 'target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis--sources.jar'
- path: 'target/stage/maven/output/build/jenesis/build.jenesis//build.jenesis--javadoc.jar'
Every release then holds build., ., -sources. and -javadoc., each with an
. and a ., and one file at
https:/ points
every kind of request at them:
module=https://github.com/jenesis/jenesis/releases/download/v{version}/{module}-{version}{-classifier}.{type}
module.latest=https://github.com/jenesis/jenesis/releases/latest/download/{module}.jar
module.suffixes=none
moduletomaven=build.jenesis:{module}
maven=https://github.com/jenesis/jenesis/releases/download/v{version}/{artifactId}-{version}{-classifier}.{type}
maven.latest=https://github.com/jenesis/jenesis/releases/latest/download/{artifactId}.pom
maven.suffixes=none
| A build asks for | Answered by | Downloads |
|---|---|---|
requires build. on the module path |
module |
build. |
requires build., resolved through Maven |
moduletomaven, then maven |
build., then the jar |
the Maven dependency build. |
maven |
the POM, then the jar |
| its sources or javadoc | module or maven |
-sources., -javadoc. |
| a signature | module or maven |
., . |
| a checksum, to check each download | the same template | ., . |
| the newest version | module., maven. |
nothing: a HEAD request that GitHub redirects |
The module template names files by module name, which works because Jenesis's artifactId is its module name.
Where the two differ, name the files the way the artifacts are named instead, with {-suffix} -
. - or publish moduletomaven alone. A release made before
its POM was attached leaves that POM to the usual repositories, if the build has any.
Implementing a client
This chapter is for the authors of tools that read the file.
The grammar
The file is read as java. reads one, with # comments and \ continuations, in UTF-8. Of a
key named twice the last value counts, and a key a reader does not know is ignored, so the format can grow:
file = *( entry / stop )
entry = key "=" value
stop = "stop=" ( "true" / "false" ) ; true, the default: no subdomain is read
key = ( "module" / "moduletomaven" / "maven" ) [ ".since" / ".suffixes" / ".latest" ]
module = location
moduletomaven = coordinate
maven = location
latest = https-uri ; beside a template naming {version}
coordinate = groupId ":" artifactId [ ":" extension [ ":" classifier ] ]
location = https-uri ; names "://": a root, or a template with placeholders
placeholder = "{" ( "groupId" / "groupPath" / "artifactId" / "module" / "-suffix"
/ "version" / "-classifier" / "type" ) "}"
since = version
suffixes = suffix *( "," suffix )
suffix = 1*( ALPHA / DIGIT ) ; "none" names a version without one
| Placeholder | maven |
module |
moduletomaven |
|---|---|---|---|
{groupId}, {groupPath}, {artifactId} |
yes | ||
{module}, {-suffix} |
yes | yes | |
{version}, {-classifier}, {type} |
yes | yes |
Finding a key
To find a key for a module name or a groupId:
- Skip a name that cannot be a domain: one label, or a label with anything but letters, digits,
_and-. - Reverse the labels into domains, shortest first, from two labels to all of them:
net.givesbytebuddy. agent bytebuddy., thennet agent..bytebuddy. net - Fetch each domain's file once per run, remembering an absent file as well as a present one. A file that cannot
be fetched - a
404, an unknown host, a proxy that cannot reach it - is absent. - Skip a domain without a file. Where a file holds the key, it becomes the answer, unless its value is a coordinate without placeholders and the domain is shorter than the name's own.
- Stop after the first file found, unless it says
stop=; the last answer found counts.false
Answering a request
- Choose the keys:
mavenfor a Maven request; for a module,modulethenmoduletomavenon the module path, or the other way round when resolving through Maven. - For a request without a version against a template, send its latest link a
HEADrequest without following redirects. A404names no version. Otherwise take theJenesis-ModuleVersionorJenesis-MavenVersionheader, or match theLocationagainst the template up to the end of the path segment holding{version}. - Check the version against
.andsince .; a request still without a version passes over a key that restricts versions.suffixes - Resolve: expand a coordinate and resolve it as a Maven artifact; ask a root as a repository, listing only the versions the key serves; fill in a template, download the file, and check it against the strongest checksum beside it.
- Where a key does not answer, try the next one, then the configured repositories.
Failing and trusting
A tool fails, naming the file, on a key without a value, a suffix that is not a word, a stop that is neither
true nor false, an unknown placeholder, a coordinate that names no artifact, a coordinate in module or a
location in moduletomaven, a latest link beside a root, a coordinate or a template without {version}, and a
latest link that leads elsewhere or names no version.
Every location is read over https once its placeholders are filled in, and a redirect is followed only to
http or https, so no file can make a tool read a local file: or jar: URI. A certificate that does not
verify fails the build. The file only says where a file comes from: a pinned checksum or a declared signature
still decides what is accepted. A domain that changes hands passes its file to the new owner, so for every
version a build pinned, the new owner can break the build but never change what it accepts; a version the build
did not pin is only as trustworthy as the domain's owner, unless a
declared signature names who must have
produced it.