Introduction

This guide walks through KReplica's API, codegen output, and common patterns. KReplica is a DTO generator, which lets you define multiple DTO variants from one interface, among other things.

Preface

If you're new to KReplica, I implore you to check the Playground as well. Since this is a code generation tool, I believe it’s very useful to see a side-by-side view of the input and codegen output.

Setup

To start, simply add the KSP and KReplica plugins to your module's build.gradle.kts file.

plugins {
    // Use a KSP version compatible with your Kotlin version
    id("com.google.devtools.ksp") version "..."
    id("io.availe.kreplica") version "5.0.0"
}

API Stability

  • Overall: Beta
  • API Annotations: Stable*
  • Codegen Output: Beta**

*It’s under consideration whether auto nominal typing should be removed. Please let me know if you found it useful.

**The core codegen output itself (sealed interface hierarchy to create DATA, PATCH, and CREATE variants) is stable. The 'local and global' variants concept, however, is considered beta. I'm currently satisfied with them, but they can be subject to change if need be.

In retrospect, KReplica should've stayed longer as a pre-1.0.0 library — and in many ways it should be considered as one. That said, the core design has stabilized by now, even if the peripheries change.

KReplica follows SemVer strictly. As such, a v6.0.0 is already planned, even though it's only the removal of an undocumented public API (it's not listed in the docs).

If fully expect KReplica to stabilize with v6.0.0 or v7.0.0, with the latter being the potential removal of the nominal typing feature. But I will hold up on marking the library as stable until it's actually tested in additional projects.

API Annotations

This section covers the 5 annotations present in KReplica. Note the first 3 annotations are the important ones, with the latter two being more niche, if not entirely optional, in nature.

@Replicate.Model

This is the primary annotation that marks an interface for DTO generation. It defines the default behavior for all properties within the interface.

  • variants: Array<DtoVariant> (Required)
    An array specifying which DTO variants to generate. Possible values are DtoVariant.DATA, DtoVariant.CREATE, and DtoVariant.PATCH.
  • nominalTyping: NominalTyping (Default: NominalTyping.DISABLED)
    When ENABLED, wraps primitive properties in type-safe value classes.
  • autoContextual: AutoContextual (Default: AutoContextual.ENABLED)
    When ENABLED, automatically adds @Contextual to non-primitive properties if the kotlinx.serialization plugin is detected.
package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant

@Replicate.Model(
    variants = [DtoVariant.DATA, DtoVariant.CREATE, DtoVariant.PATCH]
)
private interface UserProfile {
    val id: Int
    val username: String
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaCreateVariant
import io.availe.models.KReplicaDataVariant
import io.availe.models.KReplicaPatchVariant
import io.availe.models.Patchable
import kotlin.Int
import kotlin.String

/**
 * A sealed hierarchy representing all variants of the UserProfile data model.
 */
public sealed interface UserProfileSchema {
  public data class Data(
    public val id: Int,
    public val username: String,
  ) : UserProfileSchema,
      KReplicaDataVariant<UserProfileSchema>

  public data class CreateRequest(
    public val id: Int,
    public val username: String,
  ) : UserProfileSchema,
      KReplicaCreateVariant<UserProfileSchema>

  public data class PatchRequest(
    public val id: Patchable<Int> = Patchable.Unchanged,
    public val username: Patchable<String> = Patchable.Unchanged,
  ) : UserProfileSchema,
      KReplicaPatchVariant<UserProfileSchema>
}

@Replicate.Property

Provides fine-grained control over a single property, allowing you to override the settings defined in the model-level @Replicate.Model annotation.

  • include: Array<DtoVariant> (Default: empty array)
    Explicitly includes the property only in the specified variants. If used, the model-level variants are ignored for this property.
  • exclude: Array<DtoVariant> (Default: empty array)
    Excludes the property from the specified variants.
  • nominalTyping: NominalTyping (Default: NominalTyping.INHERIT)
    Overrides the model-level nominalTyping setting for this specific property.
  • autoContextual: AutoContextual (Default: AutoContextual.INHERIT)
    Overrides the model-level autoContextual setting for this specific property.
package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant
import java.util.UUID

@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.CREATE])
private interface UserAccount {
    @Replicate.Property(exclude = [DtoVariant.CREATE])
    val id: UUID

    val email: String
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaCreateVariant
import io.availe.models.KReplicaDataVariant
import java.util.UUID
import kotlin.String

/**
 * A sealed hierarchy representing all variants of the UserAccount data model.
 */
public sealed interface UserAccountSchema {
  public data class Data(
    public val id: UUID,
    public val email: String,
  ) : UserAccountSchema,
      KReplicaDataVariant<UserAccountSchema>

  public data class CreateRequest(
    public val email: String,
  ) : UserAccountSchema,
      KReplicaCreateVariant<UserAccountSchema>
}

@Replicate.Apply

Applies other annotations (like @Serializable) to the generated DTO classes. This is useful for annotations that cannot be applied to interfaces or for applying an annotation to only a subset of generated variants.

  • annotations: Array<KClass<out Annotation>> (Required)
    Specifies the annotation classes to apply to the generated DTOs.
  • include: Array<DtoVariant> (Default: empty array)
    If provided, the annotations will only be applied to the DTO variants listed in this array.
  • exclude: Array<DtoVariant> (Default: empty array)
    The annotations will be applied to all generated variants except for those listed in this array.
package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant
import kotlinx.serialization.Serializable

@Replicate.Model(variants = [DtoVariant.DATA])
@Replicate.Apply([Serializable::class])
private interface Product {
    val id: Int
    val name: String
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import kotlin.Int
import kotlin.String
import kotlinx.serialization.Serializable

/**
 * A sealed hierarchy representing all variants of the Product data model.
 */
@Serializable
public sealed interface ProductSchema {
  @Serializable
  public data class Data(
    public val id: Int,
    public val name: String,
  ) : ProductSchema,
      KReplicaDataVariant<ProductSchema>
}

@Replicate.SchemaVersion

Manually specifies a version number for a DTO schema. This is only needed if you choose not to follow the V<number> naming convention for versioned interfaces.

  • number: Int (Required)
    The integer to assign as the version number for the schema.
package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant

private interface Account {
    @Replicate.Model(variants = [DtoVariant.DATA])
    private interface V1 : Account {
        val id: Int
    }

    @Replicate.Model(variants = [DtoVariant.DATA])
    @Replicate.SchemaVersion(2)
    private interface CurrentAccount : Account {
        val id: Int
        val email: String
    }
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import kotlin.Int
import kotlin.String

/**
 * A sealed interface hierarchy representing all versions of the Account data model.
 */
public sealed interface AccountSchema {
  public sealed interface DataVariant : AccountSchema

  /**
   * --------------------------
   * |   Version 1 (V1)   |
   * --------------------------
   */
  public sealed interface V1 : AccountSchema {
    public data class Data(
      public val id: Int,
      public val schemaVersion: Int = 1,
    ) : V1,
        DataVariant,
        KReplicaDataVariant<V1>
  }

  /**
   * --------------------------
   * |   Version 2 (CurrentAccount)   |
   * --------------------------
   */
  public sealed interface CurrentAccount : AccountSchema {
    public data class Data(
      public val id: Int,
      public val email: String,
      public val schemaVersion: Int = 2,
    ) : CurrentAccount,
        DataVariant,
        KReplicaDataVariant<CurrentAccount>
  }
}

@Replicate.Hide

Disables code generation for the annotated model. This can be useful for temporarily excluding a model from the build process without deleting the source code. This annotation has no parameters.

package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant

@Replicate.Model(variants = [DtoVariant.DATA])
@Replicate.Hide
private interface InternalFeature {
    val featureId: Int
}

@Replicate.Model(variants = [DtoVariant.DATA])
private interface PublicFeature {
    val id: Int
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import kotlin.Int

/**
 * A sealed hierarchy representing all variants of the PublicFeature data model.
 */
public sealed interface PublicFeatureSchema {
  public data class Data(
    public val id: Int,
  ) : PublicFeatureSchema,
      KReplicaDataVariant<PublicFeatureSchema>
}

API Concepts

This section covers the same topics as “API Annotations,” but instead of focusing on syntax and parameters, it explores the underlying concepts and rationale.

DTO Versioning

KReplica makes API evolution safer and more explicit through its versioning system. By nesting interfaces that follow a simple V<number> naming convention (e.g., private interface V1 : MySchema), KReplica automatically groups them into a single, version-aware sealed hierarchy.

This generated sealed interface ensures that when you handle different DTOs, your when expressions can be exhaustive. If you introduce a new version (e.g., V2), the Kotlin compiler will produce an error until you explicitly handle the new version's variants, preventing runtime errors from forgotten migration paths.

If you prefer not to use the naming convention, you can achieve the same result by manually assigning a version number with the @Replicate.SchemaVersion(number = ...) annotation.

package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant

private interface UserAccount {
    @Replicate.Model(variants = [DtoVariant.DATA])
    private interface V1 : UserAccount {
        val id: Int
    }

    @Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.PATCH])
    private interface V2 : UserAccount {
        val id: Int
        val email: String
    }

    // manually assign version number
    @Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.PATCH])
    @Replicate.SchemaVersion(3)
    private interface NewModel : UserAccount {
        val id: Int
        val username: String
        val email: String
    }
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import io.availe.models.KReplicaPatchVariant
import io.availe.models.Patchable
import kotlin.Int
import kotlin.String

/**
 * A sealed interface hierarchy representing all versions of the UserAccount data model.
 */
public sealed interface UserAccountSchema {
  public sealed interface DataVariant : UserAccountSchema

  public sealed interface PatchRequestVariant : UserAccountSchema

  /**
   * --------------------------
   * |   Version 1 (V1)   |
   * --------------------------
   */
  public sealed interface V1 : UserAccountSchema {
    public data class Data(
      public val id: Int,
      public val schemaVersion: Int = 1,
    ) : V1,
        DataVariant,
        KReplicaDataVariant<V1>
  }

  /**
   * --------------------------
   * |   Version 2 (V2)   |
   * --------------------------
   */
  public sealed interface V2 : UserAccountSchema {
    public data class Data(
      public val id: Int,
      public val email: String,
      public val schemaVersion: Int = 2,
    ) : V2,
        DataVariant,
        KReplicaDataVariant<V2>

    public data class PatchRequest(
      public val id: Patchable<Int> = Patchable.Unchanged,
      public val email: Patchable<String> = Patchable.Unchanged,
      public val schemaVersion: Patchable<Int> = Patchable.Unchanged,
    ) : V2,
        PatchRequestVariant,
        KReplicaPatchVariant<V2>
  }

  /**
   * --------------------------
   * |   Version 3 (NewModel)   |
   * --------------------------
   */
  public sealed interface NewModel : UserAccountSchema {
    public data class Data(
      public val id: Int,
      public val username: String,
      public val email: String,
      public val schemaVersion: Int = 3,
    ) : NewModel,
        DataVariant,
        KReplicaDataVariant<NewModel>

    public data class PatchRequest(
      public val id: Patchable<Int> = Patchable.Unchanged,
      public val username: Patchable<String> = Patchable.Unchanged,
      public val email: Patchable<String> = Patchable.Unchanged,
      public val schemaVersion: Patchable<Int> = Patchable.Unchanged,
    ) : NewModel,
        PatchRequestVariant,
        KReplicaPatchVariant<NewModel>
  }
}

Nominal Typing

Proposed Deprecation

While automatically creating value classes for primitives seems impressive, I have come to view it as a potential antipattern. Since, I've come to believe that value types should be specified in the domain object-level, not the DTO-level. I'd instead recommend the Compile-Safe API Mapper pattern instead as a means of providing safety.

Perhaps this feature might find some use for those that prefer a "flat model" where DTOs and domain models are one and the same. If that's the case, it's nice to know that there are a number of edge cases KReplica's nominal typing can handle. For more information, check the corresponding playground example.

That said, this feature is being considered for removal. Feedback on this feature would be very much appreciated — especially if you should be fond of it.

This feature's name is actually a bit of a misnomer, a more accurate name would be "auto nominally typed primitives." What it does is that it lets you type in primitively typed (Int, Double, String, etc.) variables in your KReplica declarations, and in the codegen output, they're automatically converted to inline value classes in the codegen output.

The benefit of this is that it prevents the accidental misuse of primitive types. For example, a function signature like fun process(orderId: Long, customerId: Long) offers no protection against swapping the two IDs, as both are structurally just Long values.

Nominal typing solves this by creating distinct types that are not interchangeable. When enabled, a property like val customerId: Long becomes val customerId: CustomerId, where CustomerId is a new @JvmInline value class CustomerId(val value: Long). This provides compile-time safety, ensuring a CustomerId can never be used where an OrderId is expected.

package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.DtoVariant
import io.availe.models.NominalTyping

@Replicate.Model(variants = [DtoVariant.DATA], nominalTyping = NominalTyping.ENABLED)
private interface Order {
    val id: Long
    val customerId: Long

    @Replicate.Property(nominalTyping = NominalTyping.DISABLED)
    val totalAmount: Double
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import kotlin.Double
import kotlin.Long
import kotlin.jvm.JvmInline

/**
 * A sealed hierarchy representing all variants of the Order data model.
 */
public sealed interface OrderSchema {
  public data class Data(
    public val id: OrderId,
    public val customerId: OrderCustomerId,
    public val totalAmount: Double,
  ) : OrderSchema,
      KReplicaDataVariant<OrderSchema>
}

/**
 * VALUE CLASSES
 */
@JvmInline
public value class OrderCustomerId(
  public val `value`: Long,
)

@JvmInline
public value class OrderId(
  public val `value`: Long,
)

Applying Annotations

You can directly add annotations to KReplica declarations, either at the model-level or the property-level. However, since KReplica model declarations are interfaces, in some cases you cannot directly apply certain annotations to it. For example, the @Serializable annotation from kotlinx.serialization must be applied to a class, not an interface. Additionally, you might want to apply an annotation like @Deprecated to only the CREATE variant of a DTO, but not the DATA variant.

The @Replicate.Apply annotation solves these problems. It instructs KReplica to add specified annotations directly to the generated data classes. You can apply annotations to all variants by default or target specific variants using its include and exclude parameters.

Auto-Contextualization

The kotlinx.serialization library oftentimes requires the usage of the @Contextual annotation, which tells kotlinx.serialization that a custom serializer is available for said given type. This custom serializer is usually provided by the library's maintainer, meaning no action is needed besides inserting the @Contextual annotation.

Normally, the IDE will help you identify which variables require @Contextual. Unfortunately, kotlinx.serialization's @Serializable annotation cannot be directly applied on interfaces. This thus is why @Replicate.Apply exists in the first place.

Via @Replicate.Apply, we can still mark our KReplica annotations as @Serializable. However, this mean we no longer get IDE help to determine whether or not a property requires @Contextual.

To get around this, KReplica automatically applies the @Contextual annotations to properties if it declares that kotlinx serialization is applied to said KReplica declaration. This feature is enabled by default.

You can disable this behavior globally by setting autoContextual = AutoContextual.DISABLED in the @Replicate.Model annotation, or control it on a per-property basis using @Replicate.Property.

package io.availe.demo.playground

import io.availe.Replicate
import io.availe.models.AutoContextual
import io.availe.models.DtoVariant
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.time.Instant

@Replicate.Model(
    variants = [DtoVariant.DATA],
    autoContextual = AutoContextual.ENABLED
)
@Replicate.Apply([Serializable::class])
private interface Event {
    val id: Int
    val timestamp: Instant

    @Replicate.Property(autoContextual = AutoContextual.DISABLED)
    @Contextual
    val manualTimestamp: Instant
}
// Generated by KReplica. Do not edit.
package io.availe.demo.playground

import io.availe.models.KReplicaDataVariant
import java.time.Instant
import kotlin.Int
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable

/**
 * A sealed hierarchy representing all variants of the Event data model.
 */
@Serializable
public sealed interface EventSchema {
  @Serializable
  public data class Data(
    public val id: Int,
    public val timestamp: @Contextual Instant,
    @Contextual
    public val manualTimestamp: Instant,
  ) : EventSchema,
      KReplicaDataVariant<EventSchema>
}

Understanding the Generated Code

This section covers the codegen output of KReplica, and how to understand it.

Schemas

In KReplica, an unversioned schema is a sealed interface that defines all possible DTO shapes (such as Data, Patch, and Create) for a given model.

Meanwhile, a version schema uses a two-layer nested model. The outer layer contains all the inner schemas, with each inner schema being a separate version. By convention, each inner schema follows a a V<number> naming convention, but you can assign custom names as long as a version number is provided with @SchemaVersion.

For example, here is how you might access the DATA variant of a generated UserAccountSchema:
  • Unversioned: UserAccountSchema.Data
  • Versioned: UserAccountSchema.V1.Data

The Patchable Wrapper

When handling PATCH or update requests, a common challenge is distinguishing between fields that were omitted (and should remain unchanged) and fields that were explicitly set to null. For example, if your DTO includes a property like description, how can you tell whether the client wanted to update it, clear it, or leave it as is? Making the property nullable introduces ambiguity: does null mean “clear this field” or “don’t change it”?

KReplica solves this with the Patchable<T> sealed class. In KReplica PatchRequest variants, all properties are wrapped in Patchable<T>, making it explicit whether a field should be updated, cleared, or left unchanged.

By default, each property in a PatchRequest is set to Patchable.Unchanged, so you only need to specify the fields you want to update.

This wrapper provides two explicit states:

  • Patchable.Set(value) Use this to update a property with a new value. For example, if you have val email: Patchable<String> you can set it null with Patchable.Set(null).
  • Patchable.Unchanged Use this to leave a property unchanged. This is the default for all properties in a PatchRequest and signifies the property was not sent and should be ignored during the update.

If a schema is marked as serializable, KReplica will automatically utilize the serializable version of this wrapper.

Local Variants

For each KReplica schema, KReplica generates “local variant” marker interfaces: DataVariant, CreateRequestVariant, and PatchRequestVariant. Local variants are only generated as needed. For example, if the schema does not include a CREATE variant, it will not generate the CreateRequestVariant local variant.

This allows for exhaustive when expressions in your code.

Global Variants

Every schema generated by KReplica includes a “global” variant interface, such as KReplicaDataVariant<V>. Unlike local variants, which are tied to a specific schema, global variants are shared across all KReplica schemas.

This makes it possible to write generic code that works with any KReplica-generated model.

Core Patterns & Use Cases

These are some recommended patterns. "Patterns" are not part of KReplica directly, but instead show how you can use Kotlin features with KReplica.

Exhaustive `when` Statements

A neat feature of Kotlin is its exhaustive when expressions: when you use a sealed class or sealed interface, the compiler forces you to handle every possible subtype.

KReplica takes advantage of this by generating DTOs like:

public sealed interface <SchemaName>Schema

This design is especially helpful when making use of versioned DTOs.

You can write when statements that are:

  • Version-specific – handle all variants for a single version (e.g., all V2 DTOs).
  • Variant-specific – handle the same variant type across all versions (e.g., all Data types).
  • Schema-wide – handle every possible DTO in the schema.

kotlinx.serialization Integration

Integrating KReplica with kotlinx.serialization is straightforward, but it requires the @Replicate.Apply annotation as @Serializable cannot be applied directly to interfaces.

You can tell KReplica to serialize a DTO by simply typing: @Replicate.Apply([Serializable::class]).

package io.availe.demo.patterns

import io.availe.Replicate
import io.availe.models.DtoVariant
import kotlinx.serialization.Serializable

@Replicate.Model(variants = [DtoVariant.DATA, DtoVariant.CREATE])
@Replicate.Apply([Serializable::class])
private interface Product {
    @Replicate.Property(exclude = [DtoVariant.CREATE])
    val id: Int
    val name: String
    val price: Double
}
// Generated by KReplica. Do not edit.
package io.availe.demo.patterns

import io.availe.models.KReplicaCreateVariant
import io.availe.models.KReplicaDataVariant
import kotlin.Double
import kotlin.Int
import kotlin.String
import kotlinx.serialization.Serializable

/**
 * A sealed hierarchy representing all variants of the Product data model.
 */
@Serializable
public sealed interface ProductSchema {
  @Serializable
  public data class Data(
    public val id: Int,
    public val name: String,
    public val price: Double,
  ) : ProductSchema,
      KReplicaDataVariant<ProductSchema>

  @Serializable
  public data class CreateRequest(
    public val name: String,
    public val price: Double,
  ) : ProductSchema,
      KReplicaCreateVariant<ProductSchema>
}

Note KReplica has auto-contextualization enabled by default. You can disable this model-wide or on a per-property basis if needed. Disabling it is likely the best practice if you intend to use @Serializable(with = ...) instead of @Contextual.

Compile-Safe DTO Mapper

KReplica only generates DTOs, so how you implement mapping is left up to you. However, here is a mapping pattern which I'm keen to. The point is to ensure compile-time guarantees in this specific scenario: You utilized Kreplica to generate the 3 DTO variants (data, create, patch) for a CRUD API. Furthermore, there is a specific `id` which can be used for mapping.

Now, the entire point of the generic interface below (check "The Reusable Example" in the code snippet below) is to create a systematized approach that implements three methods:

  • toDataDto: Maps domain object -> the DATA variant
  • toDomain: Maps the DATA variant -> Domain object
  • applyPatch: Maps the PATCH variant -> Domain object

Note that in the example below, the V type parameter prevents mixing DTOs from different schema versions at compile time. For versioned schemas, use something like UserAccountSchema.V1 or UserAccountSchema.V2. For unversioned schemas, use the schema type itself, such as UserAccountSchema. This keeps variants aligned and blocks from occurring.

For this pattern, I strongly discourage nullable properties. KReplica’s three variants are meant to reduce the need for nullables by including only the fields relevant to each operation. For example, say that you have an immutable `id` that's created by a database, but not directly by your program. Only the DATA variant should have access to the `id` field, not the CREATE or PATCH variants.

This is because in this example, the Kotlin compiler forces you to map required properties, but allows you to skip nullable properties. And forgetting to map a field can lead to inadvertent data loss. If you need to model a field which may or may not be present, I'd recommend using an option type instead of a nullable. Note Kotlin/KReplica does not have an option type built in — as, although I recommend this pattern, this is outside the immediate scope of the library.

If it helps, the 1st tab ("The Reusable Pattern") is the generic interface that is the actual pattern. Tabs 2 and 3 are provided for context, but are not that important (they show a mock domain model and mock KReplica declaration). The 4th tab ("The Implementation") shows an example of how you can actually apply the pattern shown in the 1st tab.

Frequently Asked Questions

Can a @Replicate.Property have a broader replication than its @Replicate.Model?

No. The restriction of all properties must be a subset of the parent model's variants. This ensures fail-fast feedback. If you restrict a parent's replication but forget to update a child property, you'll get an immediate build-time error instead of a silent failure.

If a @Replicate.Model has another @Replicate.Model as a field, does the order of compilation matter?

No. KReplica uses a two-pass compilation strategy. It first generates stub files for all @Replicate.Model declarations to make their types known, then performs the main compilation. This ensures that nested models resolve correctly regardless of file order.

Why do all the examples use the private keyword (private interface)?

The private keyword is not required, but it's a recommended practice. The source interfaces are only for KReplica's use; your application code will interact with the generated DTOs. Making them private prevents them from polluting the global namespace. This is especially useful for versioned schemas, as it allows you to nest versions (e.g., private interface V1 : UserAccount) inside a parent scope, avoiding naming collisions.