gof-prototype · git:20260828.5f76990 · 2026-08-28 · sha256 bbf8c7664e3e20cc

gof-prototype git:20260828.5f76990A

Immutable. This exact content never changes and is served at /api/v1/blob/bbf8c7664e3e20cc.

---
name: gof-prototype
description: >
  Prototype in modern Java: producing a new object from an existing instance's state, when the
  configuration is expensive or the concrete type is unknown to the caller. Covers why
  Cloneable/clone() is a broken contract and what replaces it, the deep-versus-shallow decision
  on graphs with identity and cycles, why immutability removes the need to copy at all, the
  torn-copy hazard under concurrency, and the identity rules when copying persisted objects. Use
  when clone() or Cloneable appears, when an object is duplicated by serialising and
  deserialising it, when a configured template must be instantiated many times, when a JPA entity
  is copied with its id still set, or when a "copy" turns out to share a mutable list with its
  original. Does not cover constructing from parameters (gof-builder), selecting a type to create
  (gof-factory-method), sharing rather than copying (gof-flyweight), or snapshot semantics for
  undo (gof-memento).
---

# Prototype

## Purpose

Create a new object by copying a configured one. The pattern applies when the state that makes
an object useful was assembled at runtime and cannot be re-derived from parameters — a document
template, a pre-wired processing pipeline, a scenario fixture — or when the copier does not know
the concrete class it is duplicating.

In modern Java the pattern is mostly a warning. Two forces have eroded it: immutable values need
no copies at all, and Java's built-in copying mechanism (`Cloneable`) is a broken contract that
Effective Java advises against. What survives is narrow, and it should be implemented with copy
constructors or copy factories, never with `clone()`.

## When it is the answer

```text
An object's configuration is assembled at runtime and duplicating it
is cheaper or more reliable than re-deriving it
        → Prototype, via a copy factory.

The set of things to instantiate is registered by name at runtime and
the registry does not know their classes
        → a registry of prototypes, each able to copy itself.

A mutable working object must be duplicated so two paths can diverge
(a scenario, a draft, a what-if calculation)
        → Prototype — and consider making the type immutable instead,
          which removes the need entirely.
```

## When it is not

- **The object is immutable.** There is nothing to copy: share the instance. This eliminates the
  majority of proposed uses (`java-immutability`).
- **The state can be re-derived from parameters.** Then a factory or builder is clearer, and the
  new object does not inherit whatever the source accumulated.
- **The concrete type is known.** A copy constructor is more discoverable, type-safe and
  documentable than a polymorphic `copy()`.
- **Only a few fields differ from the original.** Records' `withX` methods express "the same but
  for X" directly, without a copy step that must be kept in sync with the field list.
- **The object is an entity with identity.** A copy of an entity is a _different_ entity; see
  the identity rules below before duplicating anything with an id, a version or a lifecycle.

## Modern Java expression

```text
Do not                              Do
──────────────────────────────────  ─────────────────────────────────────
implements Cloneable                a copy constructor:
Object clone()                        Config(Config other)
                                    or a static copy factory:
                                      static Config copyOf(Config other)

deep copy via serialise/deserialise an explicit copy that names each
                                    field, so a new field is a compile
                                    error rather than a silent share

polymorphic clone() on a hierarchy  an abstract copy() returning the
                                    interface type, implemented per
                                    subtype — a covariant, documented
                                    contract you control

"copy then mutate two fields"       record + withX(), or a builder seeded
                                    from the original
```

`clone()`'s specific defects — `Cloneable` declares no `clone` method, the protected modifier
forces every subclass to cooperate, the default is a field-for-field shallow copy that bypasses
constructors so `final` fields cannot be reassigned, and no subclass can be trusted to have
implemented it correctly — are why every replacement above is preferred. Details in
[references/copying-in-java.md](references/copying-in-java.md).

## Decision rules

```text
IF the type is immutable
THEN do not copy. Share the reference.

IF the copy shares any mutable substructure with the original
THEN it is not a copy; it is an alias with two names. Decide, per field,
     whether sharing is intended, and write it down.

IF the graph contains cycles or object identity is meaningful
THEN a naive deep copy either loops forever or duplicates shared nodes.
     Use an identity map keyed by the original node, or refuse to copy.

IF the source can be mutated while it is being copied
THEN the copy can be internally inconsistent. Copy under the same lock
     the mutators use, or snapshot into an immutable value first.

IF the object has a persistent identity (@Id, a version, a natural key)
THEN the copy must get a new identity and a reset version, and any
     lifecycle timestamps must be re-stamped. Copying the id produces a
     duplicate-key failure at best and a silent overwrite at worst.

IF copying is done by serialising and deserialising
THEN it is slow, silently deep, ignores transient fields' meaning, and —
     with any input you did not produce — is a remote code execution
     surface. Write the copy explicitly.

IF a new field is added to the type
THEN the copy must fail to compile or the copy is now wrong. Prefer a
     copy constructor that lists fields over reflection that guesses.
```

## Cross-cutting checks

- **Concurrency.** Copying a mutable object is a multi-field read and is not atomic. Another
  thread mutating the source mid-copy yields a "copy" that never existed — fields from before
  and after the change. Either copy while holding whatever lock guards the source, or have the
  source expose an immutable snapshot and copy that. A `copy()` documented as thread-safe with
  no synchronisation is documentation, not safety (`java-memory-model`).
- **Distribution.** Copying a DTO is not copying the entity it represents; the copy shares no
  identity, no version and no server-side state. Where a prototype is transmitted, the receiving
  process reconstructs it from bytes — which is deserialisation, with its own trust boundary,
  not this pattern. Never build a prototype registry keyed by class names supplied by a remote
  peer.
- **Performance.** "Copying is faster than constructing" is an assumption, not a fact: a deep
  copy allocates the whole graph again and defeats escape analysis, while construction of a
  simple object is one of the cheapest things the JVM does. Justify a prototype by the
  _configuration_ being expensive to reproduce, not by allocation cost — and if the claim is
  about cost, measure it (`allocation-profiling`).
- **Testing.** A shared mutable prototype used as a test fixture is a cross-test dependency: one
  test mutating the copy's shared substructure changes another test's data. Prototype fixtures
  must be deep-copied, or be immutable, or be rebuilt per test.

## Review checklist

- [ ] `Cloneable` and `clone()` are absent; copying is a constructor or a named factory
- [ ] Every field is accounted for: copied, deliberately shared, or deliberately reset
- [ ] Adding a field breaks compilation of the copy, rather than silently sharing it
- [ ] Mutable collections and arrays are copied, not aliased
- [ ] Identity fields — id, version, created-at, correlation ids — are reset, not copied
- [ ] Copying under concurrency is either locked or performed on an immutable snapshot
- [ ] No copy is implemented by serialisation round-trip
- [ ] The type is not simply immutable, in which case the copy should not exist

## References

- [Copying in Java](references/copying-in-java.md) — why `Cloneable` is broken in detail, copy
  constructors against copy factories against wither methods, the deep-versus-shallow decision
  table, cycles and identity maps, the serialisation round-trip's costs and security surface,
  and the rules for copying JPA entities. Read before implementing any copy.
- [Worked example](references/worked-example.md) — a registry of configured document templates
  instantiated per request: the `Cloneable` version and its two defects, the copy-factory
  version, identity reset when the copy is persisted, and the snapshot that makes copying safe
  under concurrency. Read when implementing.