Skip to content

Adopt multi-module layout for independent versioning (start by breaking the taskflow ↔ artifactadapter cycle) #36

Description

@mushrafmim

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type/ImprovementEnhancement to existing functionality

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions