Publishing Your First Java Library to Maven Central (Using “Add Two Numbers” as an Example)
If you’ve ever published a package to npm, you know the drill: npm login, npm publish, done in thirty seconds. Publishing to Maven Central is... not that. It's a much heavier process, built around verified identity, cryptographic signing, and immutable releases which is great for trust and terrible for your first afternoon trying to do it.
This post walks through the entire process end to end, using the simplest possible library: a function that adds two numbers. The goal isn’t the code it’s making sure you understand every piece of ceremony around it, so you can apply this to literally any Java library you build afterward.
Why Maven Central is so much stricter than npm
Before diving in, it helps to know why each step exists:
- Namespace ownership Maven Central won’t let you publish under
com.google.whateverunless you actually controlgoogle.com. This stops squatting and impersonation. - GPG signing every artifact must be cryptographically signed, so anyone downloading your jar can verify it really came from you and wasn’t tampered with in transit.
- No
-SNAPSHOTreleases, and once published, releases are immutable you can't quietly push a broken update over an existing version like you sometimes can elsewhere. If something's wrong, you ship a new version.
None of this is arbitrary it’s what lets millions of projects pull in Maven dependencies without a “supply chain attack of the week” problem. But it does mean the first release takes real setup.
Step 0: The project
Let’s keep this deliberately trivial so the process is the star of the show:
package io.github.yourusername.mathlib;public class MathLib {
public static int addTwoNumbers(int a, int b) {
return a + b;
}
}A pom.xml to match:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion><groupId>io.github.yourusername</groupId>
<artifactId>mathlib</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging> <name>MathLib</name>
<description>A tiny demo library that adds two numbers.</description> <properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
</project>
That’s the whole “product.” Now the actual work begins.
Step 1: Create a Sonatype Central account and claim a namespace
Go to central.sonatype.com and sign up (GitHub login is the fastest route).
Your groupId has to map to something you can prove you own. You have two realistic options:
- You own a domain (e.g.
yourcompany.com) → claimcom.yourcompanyby adding a DNS TXT record with a verification code Sonatype gives you. - You don’t own a domain → claim
io.github.<your-github-username>instead. Verification here is instant — Sonatype just confirms you control that GitHub account.
For a demo library (or honestly, most personal projects), option 2 is the easy and completely legitimate path. That’s what the groupId above assumes.
Step 2: Generate a GPG signing key
This is the part that trips people up the most, so slow down here.
Install GPG if you don’t have it — Gpg4win on Windows, brew install gnupg on Mac, or it's likely already on Linux.
Generate a key:
gpg --full-generate-keyAnswer the prompts: default key type, 4096 for key size, no expiry (default), your name and email, and a passphrase you'll actually remember — you need it every time you sign a release.
Get Raj Dhok’s stories in your inbox
Join Medium for free to get updates from this writer.
Find your key ID:
gpg --list-keysYou’ll get something like:
pub rsa4096 2026-07-26 [SC]
ABCD1234EFGH5678IJKL9012MNOP3456QRST7890
uid Your Name <you@email.com>Upload it to a public keyserver so Central can verify it:
gpg --keyserver keyserver.ubuntu.com --send-keys ABCD1234EFGH5678IJKL9012MNOP3456QRST7890A common snag here: maven-gpg-plugin sometimes throws Could not determine gpg version even when GPG is installed correctly — usually a mismatch between how the plugin parses gpg --version output and what your GPG build actually prints, or simply an old terminal session that hadn't picked up a PATH change. Opening a fresh terminal fixes it more often than you'd expect.
Step 3: Fill in the pom.xml metadata Central requires
Central rejects poms missing license, developer, and source-control info. Add:
<url>https://github.com/yourusername/mathlib</url><licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses><developers>
<developer>
<name>Your Name</name>
<email>you@example.com</email>
</developer>
</developers><scm>
<connection>scm:git:https://github.com/yourusername/mathlib.git</connection>
<developerConnection>scm:git:ssh://git@github.com/yourusername/mathlib.git</developerConnection>
<url>https://github.com/yourusername/mathlib</url>
</scm>
Push your actual code to that GitHub URL before deploying — Central’s validation checks that the SCM link resolves to something real.
Step 4: Add the release profile
This is the block that does the heavy lifting: builds a sources jar, a javadoc jar, signs everything, and uploads to Central. Wrap it in a profile so it only runs when you explicitly ask for it — normal mvn test or mvn install stays fast and untouched:
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar-no-fork</goal></goals>
</execution>
</executions>
</plugin><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin> <plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.11.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>false</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Note autoPublish=false — this uploads and validates your bundle but stops short of making it permanently public. You get one last look before flipping it live. Good insurance for a first release.
(Plugin versions drift — check Maven Central for whatever’s current before you copy these verbatim.)
Step 5: Give Maven your credentials
Generate a token from your Sonatype account page (“Generate User Token”) — this is not your login password, it’s a separate generated credential pair.
Add it to ~/.m2/settings.xml (create the file if it doesn't exist):
<settings>
<servers>
<server>
<id>central</id>
<username>YOUR_TOKEN_USERNAME</username>
<password>YOUR_TOKEN_PASSWORD</password>
</server>
</servers>
</settings>The <id>central</id> here has to match publishingServerId in the pom's release profile exactly, or Maven won't know which credentials belong to which server.
Step 6: Ship it
mvn clean deploy -P releaseThis compiles, runs your tests, builds source and javadoc jars, signs everything (you’ll be prompted for your GPG passphrase), and uploads the bundle to Sonatype.
If it succeeds, head to central.sonatype.com → Deployments, glance over what got uploaded, and click Publish.
How long until it’s actually usable?
Roughly:
- Upload: seconds to a couple minutes
- Automatic validation: a few minutes, occasionally up to ~15
- Clicking Publish: instant trigger, but full propagation to Central’s CDN can take anywhere from 10 minutes to a few hours
So end to end, budget somewhere under an hour for a clean first release.
The payoff
Once it’s live, anyone can pull your two-line addTwoNumbers function into their project with:
<dependency>
<groupId>io.github.yourusername</groupId>
<artifactId>mathlib</artifactId>
<version>0.1.0</version>
</dependency>And every release after this first one is just:
mvn clean deploy -P releaseNo more account setup, no more GPG key generation, no more pom scaffolding. The first release is the expensive one — everything after it is nearly as fast as npm publish was the whole time.
If you hit “Could not determine gpg version,” “namespace not verified,” or a version mismatch on any of these plugins, it’s almost always one of: a stale terminal that hasn’t picked up a PATH/env change, a groupId/namespace mismatch, or a plugin version that's since been superseded. Check Maven Central directly for current plugin versions rather than trusting anything you copy from a year-old blog post — including this one.







