The Forking Model

This page explains how a fork works: the modes you can choose, the lineage that’s recorded on the result, and the contracts an entity implements to participate.

Fork modes

Every fork is parameterised by a ForkMode — the direction the data is moving.

Fork modes
Mode Meaning

ToCopy

Same period as the source. Useful for cloning or "save as" without advancing.

ToNext

Target period is source + 1. The standard period-boundary carry-forward.

ToPrevious

Target period is source - 1. Less commonly used.

The library calls this dimension year in its API (e.g. sourceYearMatchesTargetYear) because that’s the canonical period it was first built for, but nothing in the contract demands a calendar year — any monotonically-increasing integer period works.

Services typically check the relationship before invoking the fork:

if (!forkFrom.getForkMode().sourceYearMatchesTargetYear(source.getSeason(), target.getSeason())) {
    throw Problem.of(ForkMode.toProblemCode(forkFrom))
        .withInfo("sourcePeriod", source.getSeason(), "targetPeriod", target.getSeason())
        .toThrowable();
}

When something goes wrong the matching ProblemCode is selected via ForkMode.toProblemCode(forkSource), which maps each mode onto a dedicated entry from ForkYeahProblemCode (see Error Codes).

Lineage

Every forkable entity carries a ForkSources field that records where the entity came from.

How lineage builds up across forks

ForkSources has two slots:

copiedFrom

Set when the entity was produced via ToCopy — i.e. is an exact copy of another entity in the same year.

continuedFrom

Set when the entity was produced via ToNext (and eventually ToPrevious) — i.e. progressed from a previous/next year.

The two slots are not mutually exclusive: a ToCopy of an entity that was previously continuedFrom 2024 keeps the continuedFrom reference and adds a new copiedFrom reference. This is what makes the lineage robust — copying a copy of a continuation still tells you which period boundary the data ultimately came from.

When you only need the most direct source, use actual():

ForkSource source = entity.getForkedFrom().actual();
// Returns copiedFrom if set, otherwise continuedFrom.

A single ForkSource carries:

  • the ForkMode that produced this entity,

  • the SourceIdentifier of the source entity,

  • the forkedAt timestamp.

Lineage is produced automatically by ForkSourcesAware.toForkSources(forkMode), so implementations rarely build it by hand. They just call original.toForkSources(ForkMode.ToNext) while constructing the new instance.

The contracts

Entity types opt into forking by implementing a small set of interfaces:

Forkable<F>

Provides fork() returning a Forker<F>. Also offers a convenience fork(ForkMode) that dispatches to the right Forker method.

ForkSourcesAware

Adds getForkedFrom(), plus default helpers toIdentifier(), resolveParentIdentifier(), toSourceIdentifier(), and toForkSources(ForkMode). Implementations override toIdentifier() (and optionally resolveParentIdentifier()); the rest is derived.

ForkRefAware<SELF>

Adds toForkRef(ForkMode) — produces a reference-only shell of the entity (UUID + lineage, no payload). Used when the new entity needs to point at another forkable entity without deep-copying it.

ForkAware<F>

The composite interface that extends all three above. Implement this when you want the full contract.

The other side of a fork is the initiator:

ForkEnabled

Marker for command/request types that initiate a fork. Provides getForkFrom() returning a ForkSource.