Class VersionedCodec

java.lang.Object
org.jwcarman.codec.versioned.VersionedCodec

public final class VersionedCodec extends Object
Prefixes every payload with a small self-describing header naming the codec that produced it, and dispatches on that header when decoding — so an application can change its storage strategy without rewriting what it has already stored.

The frame is three bytes: two magic bytes (0xC0 0xDC), then an unsigned version in 1..255, then the bytes the codec registered at that version produced.

Codec<Person> codec = VersionedCodec.<Person>builder()
        .version(1, jacksonFactory.create(Person.class))
        .version(2, foryFactory.create(Person.class).andThen(new ZstdCodec()))
        .writing(2)
        .build();

The write version is named explicitly rather than inferred from the highest registration, because that is what makes a two-phase rollout possible: deploy with the new version registered but still .writing(1), let that reach every instance, then flip to .writing(2) in a second deploy. No instance is ever handed data it cannot read.

Ordering matters. Anything layered outside a versioned codec must be undone before the header can be read, so it is frozen for the life of the store:

// header outermost: backend, transforms, everything inside may differ per version
Codec<Person> versioned = VersionedCodec.<Person>builder()
        .version(1, jackson.create(Person.class).andThen(new GzipCodec()))
        .version(2, fory.create(Person.class).andThen(new ZstdCodec()))
        .writing(2)
        .build();

// header now inside gzip: gzip must be undone to read it, so gzip can never change
Codec<Person> frozen = versioned.andThen(new GzipCodec());

Put whatever you might want to change inside the versioned codec; put only permanent commitments outside it.

Because the builder is generic over Codec, versioned byte transforms are simply the T = byte[] case — a compression or key-wrapping layer that can be swapped under an unchanged value codec.

Built codecs are immutable and thread-safe whenever their delegates are, which the Codec contract already requires. Builders are not thread-safe.

Failures. decode throws VersionedFormatException (an InvalidPayloadException) when the buffer is shorter than the three-byte header or the magic does not match — bytes some other codec wrote — and UnknownVersionException (an UnsupportedFormatException, carrying the version) when the framing is valid but names a version this codec has no registration for. The two share no parent below CodecException, so a policy that quarantines invalid payloads cannot accidentally discard a newer writer's output. Exceptions thrown by a delegate codec propagate unchanged. encode throws NullPointerException on a null value; wrap the built codec with Codec.nullSafe() to opt into passing null straight through instead.

  • Method Details

    • builder

      public static <T> VersionedCodec.Builder<T> builder()
      Starts building a versioned codec.
      Type Parameters:
      T - the type the codec converts
      Returns:
      a new builder