Portal Java Monorepo
Status
Proposed on 2026-09-16. Not yet implemented.
This is the optional Phase 0 of Portal Service Consolidation Into Rust. It is also useful on its own if the Rust migration is delayed.
Decision
Move every portal *-command and *-query repository, plus hybrid-command
and hybrid-query, into the existing light-portal repository as Maven
modules. Keep full git history.
Nothing changes at runtime:
- Each service module still builds its own small jar with the same
artifactId, for exampleuser-command-<version>.jar. hybrid-commandandhybrid-querystill build shaded server jars and Docker images, and still load service jars from/service/*.- Handler IDs,
spec.yaml, config, events, and the database are unchanged.
What changes is how the code is stored, built, versioned, and released.
Context
Current State
Measured on 2026-09-16:
- 29
*-commandand 29*-queryrepositories, plushybrid-command,hybrid-query, andlight-portal(common-util,command-common,db-provider). All are at version2.3.8-SNAPSHOT. - No service module depends on another service module. Each depends only on
light-4j and the three
light-portallibraries. - Each service
pom.xmlis about 420 to 440 lines, almost all duplicated: repositories, properties, plugin versions, javadoc, and source setup. - Every service build creates a javadoc jar during
package. Foruser-commandthis jar is about 4.3 MB, while the real jar is about 100 KB. light-portal/db-providerandcommand-commonare shaded intohybrid-command.jarandhybrid-query.jar. A change to them requires new hybrid images, not just new service jars.- Builds and releases are driven by per-repository scripts in
devops/workspace:copy-service-local.sh,copy-service-dev.sh,copy-service-test.sh: usemgitstatusto find changed repositories, runmvn clean installin each, then copytarget/<name>-*.jarintohybrid-*/service.publish.sh: runsmvn clean install deploy -DperformReleasein about 60 directories, one after another.update-asset.sh,daily-release.sh,release-docker-images.sh: reference individual repository directories.reindex-gitnexus.sh,create-gitnexus-groups.sh: index per repository.
- Eight repositories (
light-portal,config-command,config-query,deployment-command,instance-command,instance-query,oauth-command,oauth-query) have a GitHubsyncbranch workflow: a customer pushes updates tosync, andsync-auto-pr.ymlopens a PR tomaster.oauth-queryalso hasmaster-to-sync.yml.
Problems This Solves
- Atomic changes. A feature touching
db-provider, a command handler, and a query handler becomes one commit and one PR instead of three to five. - No install ordering or stale SNAPSHOTs. Today
light-portalmust bemvn installed before its dependents, or they build against an old SNAPSHOT from~/.m2or Central. Inside one reactor build, Maven resolves module dependencies from source. - AI agents see the whole contract. One checkout, one
CLAUDE.md, one GitNexus index, and one test command cover the persistence method, the handler, and itsspec.yaml. - Less build configuration. About 25k lines of duplicated POM move into one parent. A light-4j or Jackson upgrade becomes a one-line change.
- Faster daily builds. Javadoc and source jars move into the release
profile, and the reactor builds modules in parallel (
-T 1C). - Simpler scripts. Change detection per repository and the long
publish.share replaced by reactor commands.
Problems This Does Not Solve
- Two languages: Java and Rust are still maintained side by side.
- Memory use of the JVM services.
- The separate
light-portal-eventdata repository (events, not code). It is out of scope.
Target Layout
light-portal/
pom.xml # aggregator + parent (packaging pom)
CLAUDE.md, AGENTS.md # merged agent guidance
common-util/ # unchanged location
command-common/ # unchanged location
db-provider/ # unchanged location
command/
pom.xml # command parent: shared deps (rpc-router, email, ...)
attribute-command/
category-command/
...
workflow-command/
query/
pom.xml # query parent
attribute-query/
...
workflow-query/
hybrid/
hybrid-command/ # shaded server jar + docker/
hybrid-query/
dist/
pom.xml # collects service jars for deployment (see below)
.github/workflows/
The three library modules stay where they are so light-portal history and
paths do not change.
POM Structure
- Root
pom.xml(net.lightapi:light-portal, packagingpom):<version>${revision}</version>withrevision=2.3.8-SNAPSHOT, usingflatten-maven-pluginso published POMs have concrete versions.- All shared
properties,repositories,distributionManagement,dependencyManagement(light-4j, Jackson BOM, CloudEvents, JUnit), andpluginManagement. releaseprofile (activated by-DperformRelease): sources, javadoc, and gpg. Javadoc is not built by default.
command/pom.xmlandquery/pom.xml: dependencies every handler module in that group uses, so each service POM lists only what is special to it.- Service
pom.xml: parent reference,artifactId,name,description, and any extra dependencies. Target size: about 20 to 40 lines. hybrid-commandandhybrid-query: keep the shade setup. Their Docker build context stays the module directory.
Deployment Output: Small Jars Remain
Each service module still produces target/<artifactId>-<version>.jar, and
mvn deploy still publishes each one to Maven Central under its current
coordinates, because external users depend on them.
Deployment continues to copy thin jars into hybrid-*/service.
To remove the find logic in the copy scripts, the dist module uses
maven-dependency-plugin:copy-dependencies (or an assembly) to collect them:
dist/target/service/hybrid-command/*.jar # every *-command jar
dist/target/service/hybrid-query/*.jar # every *-query jar
dist/target/hybrid-command.zip # same format install.sh downloads
dist/target/hybrid-query.zip
Scripts copy these directories as they are. light-portal-install/install.sh
keeps downloading hybrid-command.zip and hybrid-query.zip unchanged.
Independent jar deployment still works: rebuild one module with
mvn -pl command/user-command -am package and copy its jar. The only
limitation is the same as today: changes to db-provider or command-common
also need new hybrid images, because those libraries are shaded.
Everyday Commands
# Full build with tests
mvn -T 1C clean install
# One service and what it depends on
mvn -pl command/user-command -am install
# Everything affected by a db-provider change
mvn -pl db-provider -amd install
# Release
mvn -T 1C clean deploy -DperformRelease
Implementation Plan
Step 1: Freeze And Prepare (Half A Day)
- Announce a short freeze on all portal Java repositories.
- Merge or close open branches. Known examples:
config-commandhasissue29andissue32. - Confirm the
syncbranches are still clean. As of 2026-09-16, the customer’s contributions are fully merged. Recheck just before Step 2 that nosyncbranch in the eight repositories is ahead ofmaster. - Record each repository’s
masterHEAD SHA in the migration PR description.
Step 2: Import With History (Scripted, About An Hour)
For each repository, with git filter-repo in a temporary clone:
git clone [email protected]:lightapi/user-command.git /tmp/import/user-command
cd /tmp/import/user-command
git filter-repo --to-subdirectory-filter command/user-command \
--tag-rename '':'user-command-'
cd ~/workspace/light-portal
git remote add import-user-command /tmp/import/user-command
git fetch import-user-command --tags
git merge --allow-unrelated-histories --no-edit import-user-command/master
git remote remove import-user-command
- Use
command/<name>for command repos,query/<name>for query repos, andhybrid/<name>for the two hybrid repos. - The tag rename prevents tag clashes between repositories.
git log --followandgit blamekeep working in the new paths.- Remove the imported
target/,log/,mvnw*, and.pre-commit-config.yamlcopies. Keep one Maven wrapper and one pre-commit config at the root.
Do this on a branch (monorepo) so the step can be rerun from scratch.
Step 3: Parent POM And Module POMs (Half A Day To A Day)
- Move shared configuration into the root
pom.xml, then addcommand/pom.xmlandquery/pom.xml. - Rewrite each service POM down to its parent, identity, and extra dependencies. This is mechanical and can be scripted or done by an agent.
- Check that the effective POM of each module has the same dependency tree as
before: compare
mvn dependency:treeoutput before and after for every module. - Move javadoc and sources into the
releaseprofile. - Add
flatten-maven-pluginand check that one published POM has a concrete version.
Step 4: Build Parity (Half A Day)
mvn -T 1C clean installpasses, including all tests.- For each service, the class list in the new jar matches the old jar
(
unzip -ldiff, ignoringMETA-INFtimestamps). hybrid-command.jarandhybrid-query.jarcontain the same classes as before.distproduceshybrid-command.zipandhybrid-query.zipwith the same jar file names as the current release assets.- Start
light-portal-installlocally with the new assets and run the existinglight-portal-testregression suite.
Step 5: Tooling And CI (Half A Day)
| Item | Change |
|---|---|
copy-service-local.sh, -dev.sh, -test.sh | Build light-portal once; copy from dist/target/service/* |
publish.sh | One mvn clean deploy -DperformRelease |
update-asset.sh | Point GENAI_COMMAND_DIR/GENAI_QUERY_DIR at the modules, or build via -pl |
daily-release.sh, release-docker-images.sh | Build hybrid images from hybrid/hybrid-* |
reindex-gitnexus.sh, create-gitnexus-groups.sh | Remove per-repository entries; index light-portal once |
.github/workflows | One sync-auto-pr.yml and master-to-sync.yml on light-portal for the customer’s single sync branch |
| CI build | mvn -T 1C verify on PR |
| Dependabot or Renovate | One configuration at the root |
Step 6: Agent Guidance (A Few Hours)
- Merge the roughly 60
CLAUDE.md/AGENTS.mdfiles into one root file. Keep a short module-level file only where a module has real special rules (for examplegenai,config,oauth,workflow). - Document the layout, the build commands above, and the rule that changes to shared libraries need hybrid image rebuilds.
- Update the GitNexus group definitions.
Step 7: Cutover (Half A Day)
- Merge the
monorepobranch intolight-portalmaster. - In each old repository: add a final commit whose README points to the new path, then archive the repository on GitHub (read-only; history and links keep working).
- Transfer open GitHub issues to
light-portal, or close them with a link. - Remove the old directories from
~/workspaceafter confirming nothing references them (grep -racrossdevops,light-portal-install, andlight-portal-doc). - Publish one release from the monorepo to Maven Central and deploy to dev, then test. Check that a project outside the monorepo can resolve one published service jar and its parent POMs.
- Tell the
synccustomer thatlight-portalis ready and the old repositories are archived.
Total effort: about 3 to 4 working days, mostly mechanical, with most of the time in Steps 3 to 5 checks.
Rollback
Until Step 7, the old repositories are untouched and remain the source of truth.
Rolling back means deleting the monorepo branch.
After Step 7, unarchive the old repositories. Changes made in the monorepo in
the meantime can be exported back with git filter-repo --subdirectory-filter.
This should not be needed if Step 4 passes.
Risks
| Risk | Mitigation |
|---|---|
Customer sync flow breaks or loses in-flight changes | Branches are clean and the customer has agreed to move; recheck before Step 2 and tell them when cutover is done |
| Dependency tree changes silently when POMs are deduplicated | Compare dependency:tree per module; compare jar contents |
| Full build becomes slow | -T 1C, javadoc only on release, -pl ... -am for targeted builds |
| External users of Central artifacts see a change | Coordinates and jar contents stay the same; check flattened POMs have no ${revision} and that the published parent POM resolves; test one external build against the first monorepo release |
| Scripts still point at old directories | grep sweep in Step 7; archive old repositories so stale scripts fail loudly |
| Tag name clashes | --tag-rename prefix during import |
Resolved Decisions
Decided on 2026-09-16:
- Customer
syncflow. The customer’s contributions are merged and clean. After cutover, they move to onesyncbranch onlight-portal. Step 7 includes telling them the cutover is done. - Maven Central publishing continues. Some users still depend on the
published jars. Every library and service module keeps its current
groupId/artifactIdand is published on each release. The root,command, andqueryparent POMs are published too, because published module POMs refer to them. - Placeholder modules stay.
blog,news,page,form,template,document, anderrormove into the monorepo like every other module. They hold features planned for when there is capacity.