# Identity-First Model: Preserving Lazy Semantics Without Custom Events

In build and configuration systems, the same tension appears again and again:

* you want an **observable model** that consumers can subscribe to using something like `all(...)`
* you do **not** want that subscription to destroy laziness
* and you would prefer to avoid introducing a custom event bus, event ordering, and a separate lifecycle model

A practical way to solve this is to **separate identity from computed state**.

## Core idea

The idea is simple:

* one object is responsible only for **identity** and minimal selection metadata
* the actual aggregate content is obtained through **separate API calls**
* those API calls may be lazy, expensive, cached, provider-based, or computed on demand

So instead of one “fat” object, we get two layers.

### 1. Identity layer

Lightweight objects that:

* are cheap to create
* are effectively immutable
* are safe to observe eagerly
* work well as keys and subscription points

### 2. State / aggregate access layer

Separate APIs that:

* resolve or compute content from identity
* do heavy work only when needed
* preserve lazy semantics where that actually matters

This is especially useful when a collection must be replayable, but should not drag expensive materialization along with it.

---

## The problem this solves

Consider a typical Gradle-like scenario.

An adapter wants to subscribe to a collection:

```java
projections.getProjections().all(projection -> {
    ...
});
```

If `projection` is a heavy object that already contains:

* computed bindings
* providers of source sets
* derived state
* partially materialized objects

then `all(...)` starts forcing things that were supposed to stay lazy.

Typical symptoms:

* lazy semantics are weakened or broken
* coupling increases
* plugin application order starts to matter
* custom events begin to look tempting: `onCreated`, `onResolved`, `onMaterialized`

The problem is not that `all(...)` is bad.
The problem is that **too much meaning has been packed into the observable object**.

---

## The solution: observe identity, not state

If the observable collection contains only identity objects, the picture changes.

For example:

```java
public interface SourceSetProjection extends Named {
}
```

This object contains only identity:

* `name`
* perhaps a small amount of selection metadata
* but not heavy aggregate state

Now this subscription:

```java
projections.getProjections().all(projection -> {
    ...
});
```

is no longer dangerous.
It eagerly materializes only cheap keys, not the full aggregate graph.

The actual content is requested separately:

```java
Set<VariantLayerBinding> bindings = projections.getBindings(projection);
NamedDomainObjectProvider<GenericSourceSet> sourceSet =
    materializer.getSourceSet(projection.getName());
```

Those calls may remain:

* lazy
* computed
* cached
* tied to runtime lifecycle
* delegated to a dedicated materializer

---

## Example

### A problematic design

Suppose we define projection like this:

```java
public interface SourceSetProjection extends Named {
    Set<VariantLayerBinding> getBindings();
    NamedDomainObjectProvider<GenericSourceSet> getSourceSet();
}
```

This is problematic because `SourceSetProjection` is no longer just identity. It is already close to an aggregate.

It mixes:

* symbolic identity
* relation data
* runtime references into a foreign domain

Subscribing via `all(...)` now risks pulling in much more than intended.

The type says “projection”, but internally it already carries half the system.

---

### A cleaner design

Split responsibilities instead:

```java
public interface SourceSetProjection extends Named {
}
```

```java
public interface SourceSetProjections {
    NamedDomainObjectCollection<SourceSetProjection> getProjections();
    Set<VariantLayerBinding> getBindings(String sourceSetName);
}
```

```java
public interface SourceSetMaterializer {
    NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
}
```

Now the adapter flow looks like this:

```java
projections.getProjections().all(projection -> {
    Set<VariantLayerBinding> bindings =
        projections.getBindings(projection.getName());

    NamedDomainObjectProvider<GenericSourceSet> sourceSet =
        materializer.getSourceSet(projection.getName());

    // apply adapter-specific policy
});
```

What changed:

* replayable subscription is preserved
* eager observation is acceptable because `SourceSetProjection` is cheap
* expensive and computed state has moved to separate APIs
* materialization remains under the control of a single owner

---

## Why this is often better than events

When identity and state are mixed together, people quickly start inventing events:

* `projectionCreated`
* `projectionResolved`
* `sourceSetAvailable`
* `sourceSetMaterialized`

That usually happens because it becomes important to know **when exactly** an object is “ready enough”.

If the observable object contains only identity, and heavy state is obtained separately, then many of those events become unnecessary.

The architecture becomes calmer:

* an **identity registry**
* a **lookup API** for relations
* a **lazy materialization API** for heavy objects

Instead of saying:

> “When this object becomes sufficiently ready, I will react.”

you can say:

> “I can observe identity immediately, and ask for the expensive state only when I actually need it.”

This is easier to reason about, easier to test, and usually easier to evolve.

---

## What should live inside an identity object

An identity object does not have to be completely empty.
It may carry **selection metadata**, as long as that metadata is:

* cheap
* stable
* not expensive to initialize
* not turning the object into an aggregate

Typical examples:

* `id`
* `name`
* `kind`
* `type`
* domain key

What should usually stay out:

* computed aggregate content
* runtime references to foreign domains
* lazy providers of heavy objects
* derived state that can trigger premature materialization

A useful rule of thumb:

**An identity object contains selection metadata; aggregate content is obtained separately.**

---

## Why this works well with `all(...)`

`all(...)` weakens laziness only if the observed objects are themselves heavy or stateful.

If the observed objects are:

* cheap
* identity-only
* effectively immutable

then eager observation is usually acceptable.

So the real principle is:

**Eager observation of identity is often harmless.
Eager observation of computed state is not.**

That is why `all(...)` can be perfectly fine for collections of:

* `Variant`
* `Layer`
* `Role`
* `SourceSetProjection`

as long as those objects stay on the identity side of the boundary.

---

## Where this principle is especially useful

This approach is particularly effective when:

* there is replayable observation via `all(...)`
* identity objects are cheap and stable
* aggregate content may be expensive
* symbolic model and runtime model should remain separate
* you want to avoid building a custom event system

For Gradle-like models, this is often a very natural fit.

---

## When it is unnecessary

If an object is:

* small
* cheap
* and already fully represents its useful content

then splitting identity and state may be overengineering.

So this is not a universal rule. It is a tool to use when there is real tension between:

* key
* state
* computation
* runtime reference

---

## Practical conclusion

The principle can be summarized like this:

1. **Identity objects** hold only identity and cheap selection metadata.
2. **Aggregate content** is not stored inside them, but retrieved through separate API calls.
3. Those API calls may perform:

   * lazy resolution
   * caching
   * heavy computation
   * materialization on demand
4. This makes it possible to use replayable mechanisms such as `all(...)` without destroying laziness where laziness actually matters.

This is how you can combine:

* simple observation
* a clean model
* no custom event bus
* lazy materialization of heavy state

---

# Short design note version

A concise version of the same principle:

> Use identity objects as cheap, observable keys.
> Keep expensive or computed aggregate content out of them.
> Resolve that content through separate APIs on demand.
> This allows replayable observation (`all(...)`) without forcing premature materialization, and often removes the need for a custom event model.
