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.
| Mode | Meaning |
|---|---|
|
Same period as the source. Useful for cloning or "save as" without advancing. |
|
Target period is |
|
Target period is |
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.
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 eventuallyToPrevious) — 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
ForkModethat produced this entity, -
the
SourceIdentifierof the source entity, -
the
forkedAttimestamp.
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 aForker<F>. Also offers a conveniencefork(ForkMode)that dispatches to the rightForkermethod. ForkSourcesAware-
Adds
getForkedFrom(), plus default helperstoIdentifier(),resolveParentIdentifier(),toSourceIdentifier(), andtoForkSources(ForkMode). Implementations overridetoIdentifier()(and optionallyresolveParentIdentifier()); 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 aForkSource.