Java Library Pattern — Reusable JAR via GitHub Packages

1. Overview

A Java library in EMS is a standalone Maven JAR module that publishes reusable code to GitHub Packages and is consumed by one or more services as a Maven dependency. It does not run on its own, has no container image, no Kubernetes deployment, and no observability surface — those concerns belong to the consumer service.

The library set is small and load-bearing:

Library Consumed by

za.co.idealogic:event (parent POM)

event-database, admin-service, registration-portal, admin-portal, mcp-server

za.co.idealogic:wordpress-database

event-database (transitively admin-service)

za.co.idealogic:event-database

admin-service, future portals that read directly from the DB schema

za.co.idealogic:spreadsheet-importer

admin-service (the EP / result import paths)

This page is a specialisation of Microservice Pattern. It re-uses the Maven-module and CI/CD halves of that baseline and explicitly drops everything below the JAR — there is no Jib build, no Helm chart, no ArgoCD manifest, no OTel agent, no health probes, no port assignment, no Spring profile model. Portal Pattern and MCP Pattern are sibling specialisations for runtime services.

2. When to Use This Pattern

Use this pattern for any new Maven artefact intended to:

  • Be consumed as a library dependency (za.co.idealogic:<artefact>) by one or more EMS services.

  • Be published to the github-christhonie GitHub Packages registry on every push to develop (-SNAPSHOT) and on every release merge to main (release version).

  • Follow the EMS GitFlow release cadence (mvn gitflow:release-start → release PR → merge to main → tag + bump).

If the artefact runs on its own (has a main class, listens on a port, has its own deploy lifecycle), it belongs to Microservice Pattern (with Portal or MCP specialisation as appropriate) — not here.

3. Relationship to the Microservice Pattern

Concern Microservice Library

Maven module shape

Single jar artefact, parented to za.co.idealogic:event, ${revision} versioning, <repositories> block

Same shape. Every library parents to za.co.idealogic:event so the GitHub Packages publish config, GitFlow plugin, flatten plugin, deploy plugin and surefire defaults all flow through the parent-pom.

GitHub Packages publishing

<distributionManagement> inherited from parent-pom → maven.pkg.github.com/christhonie/event

Same target registry (one host repo for the whole fleet keeps consumer <repositories> blocks single-entry).

GitHub Actions wrappers

Five wrappers (push-dev, push-main, push-release, pr-non-main, manual-release-start)

Four wrappers — drop pr-non-main.yml. PR builds run via push-release.yml for RC branches; libraries don’t need the frontend/backend filter.

Reusable workflows used

test-be, maven-package, maven-deploy, publish, dependencygraph, docker-build, helm-build, argocd-update, gitflow-release-finish

Subset only: test-be, maven-package, maven-deploy, publish, dependencygraph, gitflow-release-finish. No docker-build, no helm-build, no argocd-update — libraries don’t deploy.

GitFlow release management

gitflow-maven-plugin inherited from parent-pom

Same plugin, inherited from parent-pom.

Container image

Jib build, OTel agent baked in, Docker Hub push

Not applicable. A library has no runtime image.

Helm chart, ArgoCD manifests, ports, probes, profiles

Per the microservice baseline

Not applicable.

Required repo secrets

EVENT_PACKAGE_REPO_TOKEN, DOCKER_PAT, ARGOCD_REPO_TOKEN

Only EVENT_PACKAGE_REPO_TOKEN. Docker / ArgoCD secrets are unnecessary — none of the workflows consume them.

4. Maven Module Structure

4.1. POM essentials

Every EMS library parents to za.co.idealogic:event and inherits CI-friendly versioning, gitflow-maven-plugin, flatten-maven-plugin, maven-deploy-plugin, the standard surefire/failsafe configuration, and the <distributionManagement> block. The library POM only adds what’s specific to it — its dependencies and any per-module plugin tweaks.

<parent>
    <groupId>za.co.idealogic</groupId>
    <artifactId>event</artifactId>
    <version>1.3.1</version>
</parent>

<artifactId>my-library</artifactId>
<version>${revision}</version>

<properties>
    <revision>0.1.0-SNAPSHOT</revision>
</properties>

<repositories>
    <repository>
        <id>github-christhonie</id>
        <url>https://maven.pkg.github.com/christhonie/event</url>
        <releases><enabled>true</enabled><checksumPolicy>fail</checksumPolicy></releases>
        <snapshots><enabled>true</enabled></snapshots>
    </repository>
</repositories>

<distributionManagement> is inherited from the parent-pom — do not redeclare it. The <repositories> block is duplicated by convention (matching event-database/pom.xml) so a clean checkout can resolve the parent before Maven has read its repositories list.

4.2. Bringing in Spring-Boot dependency management

event:1.3.1 does not import the spring-boot-dependencies BOM globally — each library opts in if it needs Spring-managed versions for its own deps. Libraries that consume Spring (e.g. event-database, spreadsheet-importer) add the BOM to their own <dependencyManagement>:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

${spring-boot.version} resolves from the parent-pom (currently 3.4.5 on event:1.3.x). Libraries with no Spring dependencies (a pure utility JAR) can omit this block entirely.

Reference implementations: database/pom.xml, spreadsheet-import/pom.xml.

gitflow-maven-plugin requires both a develop and a main branch on the remote with the conventional GitFlow layout. New libraries should bootstrap those branches before the first manual-release-start run.

4.3. Why one host repo for all artefacts

Every EMS library publishes to https://maven.pkg.github.com/christhonie/event regardless of which GitHub repo it lives in. This is a deliberate workaround for the way GitHub Packages binds artefacts to a Solution Architect/{repo} pair: a single host repo means consumers add one <repository> entry to their POM (or inherit it from parent-pom) instead of one per source repo. The trade-off is that the package list on christhonie/event is the union of every published artefact across the fleet — that is fine, and it mirrors what the parent-pom already does for wordpress-database and event-database.

5. CI/CD

Libraries use the same reusable-workflow set documented in Microservice Pattern § CI/CD and detailed in GitHub Actions CI/CD — they just consume a smaller subset.

5.1. Wrapper inventory

Wrapper Trigger and purpose

push-dev.yml

Push to develop → test → package → publish → deploy (-SNAPSHOT to GitHub Packages) → dependency graph

push-main.yml

Push to main (only via merged release PR) → package (prod) → publish → deploy (release version to GitHub Packages) → GitFlow release-finish

push-release.yml

Push to release/** → test only (RC verification)

manual-release-start.yml

workflow_dispatchmvn gitflow:release-start → opens release PR back to main

There is no pr-non-main.yml and no docker-build/helm-build/argocd-update jobs — a library never produces a runtime artefact.

5.2. Required repository settings

Mirror the bring-up steps in GitHub Actions CI/CD § Required Repository Settings except the Docker / ArgoCD bits that don’t apply:

  • Repo visibility = private. christhonie/event is private; GitHub blocks a public calling repo from referencing reusable workflows in a private source repo, regardless of the access-level setting. Every EMS Maven repo (event-database, wordpress-database, admin-service, …​) is private; new library repos must match. Symptom of a mismatch: push-dev.yml runs fail at 0s as startup_failure with no jobs and no log output, while a non-reusable diagnostic workflow on the same repo runs fine.

  • Default workflow permissions: Read and write (otherwise dependencygraph.yml and gitflow-release-finish.yml fail at startup with no log output).

  • Reusable workflow access on christhonie/event set to Accessible from repositories owned by the user 'christhonie' (configured once on the parent-pom repo, applies fleet-wide).

  • Repository secret EVENT_PACKAGE_REPO_TOKEN — a PAT (or fine-grained token) with read:packages + write:packages scopes. Used for both consuming SNAPSHOTs from christhonie/event and publishing the library’s own artefacts.

DOCKER_PAT and ARGOCD_REPO_TOKEN are not used by any library workflow — do not configure them on library repos.

5.3. Pipeline shape

push to develop                                push to main (merged release PR)
       │                                              │
       ▼                                              ▼
   test-be ─► maven-package                      maven-package (prod)
                  │                                   │
       ┌──────────┼──────────┐               ┌────────┼──────────┐
       ▼          ▼          ▼               ▼        ▼          ▼
   publish   maven-deploy  dependency-     publish  maven-     gitflow-release-
                            graph                   deploy     finish

5.4. Wrapper templates

The four library wrappers are small. Reference implementations live under spreadsheet-import/.github/workflows/ and database/.github/workflows/. Inputs / secrets follow the conventions documented in GitHub Actions CI/CD § Consuming Workflows.

6. Bootstrap Checklist

Step-by-step from "git init" to "first SNAPSHOT in GitHub Packages":

  1. GitHub repo created (e.g. christhonie/<library-name>) as private — must match the visibility of christhonie/event. Public-→-private callers fail with startup_failure and no log output.

  2. Repository workflow permissions flipped to Read and write (gh api -X PUT repos/christhonie/<repo>/actions/permissions/workflow -f default_workflow_permissions=write -F can_approve_pull_request_reviews=true).

  3. Repository secret EVENT_PACKAGE_REPO_TOKEN added.

  4. Branches develop and main created (gitflow-maven-plugin requires both); set develop as the default branch.

  5. Maven module scaffolded:

    1. POM with the shape from § POM essentials. Confirm <groupId>za.co.idealogic</groupId> and <distributionManagement> (or inheritance) targets https://maven.pkg.github.com/christhonie/event.

    2. Sources under src/main/java/za/co/idealogic/<package>/…​.

    3. Smoke test under src/test/java.

  6. .github/workflows/ populated with the four wrappers (push-dev, push-main, push-release, manual-release-start) — copy from spreadsheet-import or database.

  7. .github/CODEOWNERS added (* @christhonie).

  8. .gitattributes added — LF normalisation for source, CRLF for .bat/.cmd, binary for .jar/images/spreadsheets.

  9. .gitignore includes target/ and .flattened-pom.xml.

  10. Local verification: mvn clean install -DskipTests produces <artifactId>-<revision>.jar and a flattened POM under ~/.m2/repository/za/co/idealogic/<artifactId>/<revision>/ with ${revision} resolved to the literal version.

  11. First push to develop triggers push-dev.yml. Watch via gh run watch. On success, the SNAPSHOT JAR appears under https://github.com/christhonie/event/packages (the parent-pom repo’s package list, not the library’s own repo).

  12. Smoke-consume from a service: add <dependency><groupId>za.co.idealogic</groupId><artifactId>…​</artifactId><version>…​-SNAPSHOT</version></dependency> and mvn -U dependency:resolve.

For the first release follow the GitFlow flow: trigger manual-release-start.yml with the target version → review the auto-opened release PR → merge into main → push-main.yml deploys the release version, gitflow-release-finish tags main and bumps develop’s `<revision> to the next SNAPSHOT.

7. Reference Implementations

Library Parent Notes

event (parent-pom)

Shared Maven plugin, repository, and distribution settings. Hosts the reusable GitHub Actions workflows under .github/workflows/. Source: christhonie/event.

event-database

za.co.idealogic:event

The reference for an EMS-parented library: full GitFlow, JPA + Liquibase + email templates, dependency-graph submission. Source: christhonie/event-database.

wordpress-database

za.co.idealogic:event

Pre-existing WordPress schema model. Stable; rarely touched.

spreadsheet-importer

za.co.idealogic:event

Library wrapping POI for CSV/XLSX import. Imports the spring-boot-dependencies BOM in its own dependencyManagement for the Spring deps it consumes. Source: christhonie/spreadsheet-import.

8. Anti-Patterns

  • Creating the library repo as public while christhonie/event is private. GitHub blocks the cross-repo reusable-workflow call and the run dies at 0s with no log. Match the visibility of christhonie/event (private).

  • Parenting a new library to spring-boot-starter-parent (or any other non-EMS parent). You then have to redeclare ${revision}, the flatten plugin, the gitflow plugin, the deploy plugin, and <distributionManagement> — all of which are already configured on za.co.idealogic:event. Use the EMS parent and add the spring-boot-dependencies BOM in dependencyManagement if you need Spring-managed versions.

  • Publishing to a per-library host repo (e.g. maven.pkg.github.com/christhonie/spreadsheet-import). Every consumer would have to add another <repository> entry. Always publish to christhonie/event.

  • Inlining a spring-boot-maven-plugin repackage execution on a library POM. It produces an executable fat JAR alongside the library JAR and confuses consumers; library JARs are plain.

  • Adding docker-build / helm-build jobs to a library workflow. The reusable workflows will run, fail (no chart, no Dockerfile), and clutter the run history. Drop them.

  • Skipping the develop + main branch setup before the first manual-release-start. The gitflow-maven-plugin will fail with a confusing error about missing branches.

9. Further Reading

10. Change History

Date Change

2026-04-29

Initial draft. Captures the library-vs-microservice deltas, the single-host-repo convention for GitHub Packages, the four-wrapper CI/CD inventory, and the bootstrap checklist. Triggered by getting spreadsheet-importer onto CI/CD.

2026-04-29

Drop the "independent library" variant — spreadsheet-importer was reparented to za.co.idealogic:event:1.3.1, so every EMS library now uses the standard parent-pom. Replace the spring-boot-starter-parent POM template with a spring-boot-dependencies BOM-import recipe for libraries that need Spring-managed versions. Add the corresponding anti-pattern.

2026-04-29

Document the public-→-private visibility constraint for cross-repo reusable workflows (GitHub blocks the call regardless of access-level). Captured as a required setting, an anti-pattern, and a bootstrap-checklist step. Caught while bringing spreadsheet-importer onto CI/CD: a public repo + reusable-workflow uses: produced 0s startup_failure with no log, only diagnosable via a non-reusable smoke workflow.