Current Limitation
The entire repo is a single Go module (module github.com/OpenNSW/core, one root go.mod). Since a Go module is the unit of versioning, every package shares one version tag. As a result:
- Consuming repos are forced to upgrade everything together — they can't take a fix in one package (e.g.
storage) without also pulling unrelated changes in others (e.g. taskflow) under the same version bump.
- A breaking change anywhere forces a major bump that nominally affects all consumers, even those who don't import the changed package.
- There's no way to express per-package semver or changelogs.
Compounding this, the internal dependency graph contains a module-level cycle that blocks any split: artifactadapter/{tasktemplate,subtasktemplate} → taskflow/types, while taskflow/orchestrator → artifactadapter. At the package level this is a clean DAG, but any module boundary drawn around taskflow/* vs artifactadapter/* creates a cycle, which Go modules do not allow.
Suggested Improvement
Move toward a multi-module monorepo: independently-consumed subtrees get their own go.mod, versioned independently via subdirectory-prefixed tags (e.g. storage/v0.3.1, taskflow/v1.4.0). Consumers then pin each module separately, so a storage fix never moves taskflow's version.
Phased:
- Phase 1 (prerequisite, valuable on its own): Break the cycle by moving the template types (
TaskTemplate, SubTaskTemplate, ExtensionConfig, ExecutionPhase, currently in taskflow/types) into artifactadapter. Rationale: artifactadapter is the producer of these types (subtasktemplate.Load/tasktemplate.Load return them); taskflow/orchestrator only consumes them and already depends on artifactadapter. The current placement is itself a layering inversion. Result: a one-directional taskflow → artifactadapter graph. (No lower taskflow package — store, plugins — imports these types today, so the move is safe.)
- Phase 2: Introduce per-module
go.mod at consumer-facing seams (not every leaf package). Use go.work for local multi-module dev; run CI with GOWORK=off to validate real version resolution. Adopt subdirectory tag-prefix releases plus a cascade-release script for dependent modules.
Granularity guidance: group by release cadence / consumer boundary — e.g. workflow+temporal, taskflow(+artifactadapter), and split out storage/authn/authz/notification/payment only where consumed independently. Note internal/* is import-restricted and cannot be consumed externally under that path.
Version
main @ 9c090fd
Additional Context
Industry precedent for monorepo-with-independent-modules: aws-sdk-go-v2 (each service/* is its own module — visible in our own require block), k8s.io/*, google.golang.org/genproto. Two caveats to call out: (1) the module graph must stay acyclic, and (2) the cascade-release tax (a breaking change in a low-level module forces bump+release of every dependent) is the main operational cost and is commonly underestimated.
Current Limitation
The entire repo is a single Go module (
module github.com/OpenNSW/core, one rootgo.mod). Since a Go module is the unit of versioning, every package shares one version tag. As a result:storage) without also pulling unrelated changes in others (e.g.taskflow) under the same version bump.Compounding this, the internal dependency graph contains a module-level cycle that blocks any split:
artifactadapter/{tasktemplate,subtasktemplate} → taskflow/types, whiletaskflow/orchestrator → artifactadapter. At the package level this is a clean DAG, but any module boundary drawn aroundtaskflow/*vsartifactadapter/*creates a cycle, which Go modules do not allow.Suggested Improvement
Move toward a multi-module monorepo: independently-consumed subtrees get their own
go.mod, versioned independently via subdirectory-prefixed tags (e.g.storage/v0.3.1,taskflow/v1.4.0). Consumers then pin each module separately, so astoragefix never movestaskflow's version.Phased:
TaskTemplate,SubTaskTemplate,ExtensionConfig,ExecutionPhase, currently intaskflow/types) intoartifactadapter. Rationale:artifactadapteris the producer of these types (subtasktemplate.Load/tasktemplate.Loadreturn them);taskflow/orchestratoronly consumes them and already depends onartifactadapter. The current placement is itself a layering inversion. Result: a one-directionaltaskflow → artifactadaptergraph. (No lowertaskflowpackage —store,plugins— imports these types today, so the move is safe.)go.modat consumer-facing seams (not every leaf package). Usego.workfor local multi-module dev; run CI withGOWORK=offto validate real version resolution. Adopt subdirectory tag-prefix releases plus a cascade-release script for dependent modules.Granularity guidance: group by release cadence / consumer boundary — e.g.
workflow+temporal,taskflow(+artifactadapter), and split outstorage/authn/authz/notification/paymentonly where consumed independently. Noteinternal/*is import-restricted and cannot be consumed externally under that path.Version
main@9c090fdAdditional Context
Industry precedent for monorepo-with-independent-modules:
aws-sdk-go-v2(eachservice/*is its own module — visible in our ownrequireblock),k8s.io/*,google.golang.org/genproto. Two caveats to call out: (1) the module graph must stay acyclic, and (2) the cascade-release tax (a breaking change in a low-level module forces bump+release of every dependent) is the main operational cost and is commonly underestimated.