Discovery (proposal)

The file format on this page is a proposal for any Java tool to adopt, so it may still change as others take it up. Jenesis reads it where jenesis.repository.discover is set.

A Maven groupId and a Java module name are reversed domain names: net.bytebuddy belongs to bytebuddy.net. 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.util.Properties 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://<domain>/.well-known/java-repository.properties, 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.build says that the group build.jenesis is attached to the GitHub releases of Jenesis, and that those releases hold no snapshots. A dependency on build.jenesis:build.jenesis:0.15.4 then downloads https://github.com/jenesis/jenesis/releases/download/v0.15.4/build.jenesis-0.15.4.jar.

Which file answers

For net.bytebuddy.agent, a tool reads the file of bytebuddy.net - the two labels a vendor owns - and the file of agent.bytebuddy.net only where bytebuddy.net 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=false: 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.xml is merged with that of the usual repositories, so a version range sees the versions of both - 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, .sha256 or .sha1 beside it, the strongest one present, and a mismatch fails the build.

A Maven template fills in {groupId} (net.bytebuddy), {groupPath} (net/bytebuddy), {artifactId}, {version}, {-classifier} (-sources, or nothing for the plain jar) and {type} (jar, pom, jar.asc, jar.sha256). 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>.since=<version> 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: 1.2.3-rc.1 comes before 1.2.3, 1.10.0 after it.
  • <key>.suffixes=<suffix>[,<suffix>...] lists the qualifiers it serves - the part of a version after its first dash, matched by its leading word, ignoring case. none names a version without one; without the key, every version is served.

With maven.since=0.16.0 and maven.suffixes=none, 1.2.3 is served, while 1.2.3-SNAPSHOT and everything before 0.16.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>.latest. 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/latest/download/<name> to releases/download/v<version>/<name> 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.repository.discover is set, on the command line or in jenesis.properties. 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.repository.insecure allows it, and under jenesis.repository.offline 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.build to the artifact of the same name in the group build.jenesis. 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.bytebuddy, -agent for net.bytebuddy.agent. One line maps every Byte Buddy module:

moduletomaven=net.bytebuddy:byte-buddy{-suffix}

A module whose suffix names no artifact, such as net.bytebuddy.utility, 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_to_maven layout, the one a module-info.java 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://repo.jenesis.build/, asked as a configured module repository is.
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.jenesis-<version>.jar with no POM and no Maven at all. module.latest names the newest version for a requires that no pin names. A Jenesis module service may serve as the latest link too, https://repo.jenesis.build/module/{module}/{module}.jar: 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.individual has it upload a .sha256 beside each, as in this excerpt of Jenesis's own jreleaser.yml:

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.jenesis-<version>.jar, .pom, -sources.jar and -javadoc.jar, each with an .asc and a .sha256, and one file at https://jenesis.build/.well-known/java-repository.properties 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.jenesis on the module path module build.jenesis-<version>.jar
requires build.jenesis, resolved through Maven moduletomaven, then maven build.jenesis-<version>.pom, then the jar
the Maven dependency build.jenesis:build.jenesis:<version> maven the POM, then the jar
its sources or javadoc module or maven -sources.jar, -javadoc.jar
a signature module or maven .jar.asc, .pom.asc
a checksum, to check each download the same template .jar.sha256, .pom.sha256
the newest version module.latest, maven.latest 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} - .../byte-buddy{-suffix}-{version}{-classifier}.{type} - 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.util.Properties 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:

  1. Skip a name that cannot be a domain: one label, or a label with anything but letters, digits, _ and -.
  2. Reverse the labels into domains, shortest first, from two labels to all of them: net.bytebuddy.agent gives bytebuddy.net, then agent.bytebuddy.net.
  3. 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.
  4. 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.
  5. Stop after the first file found, unless it says stop=false; the last answer found counts.

Answering a request

  1. Choose the keys: maven for a Maven request; for a module, module then moduletomaven on the module path, or the other way round when resolving through Maven.
  2. For a request without a version against a template, send its latest link a HEAD request without following redirects. A 404 names no version. Otherwise take the Jenesis-ModuleVersion or Jenesis-MavenVersion header, or match the Location against the template up to the end of the path segment holding {version}.
  3. Check the version against .since and .suffixes; a request still without a version passes over a key that restricts versions.
  4. 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.
  5. 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.