# Declarative Data Specification: Why the Blueprint Form?

> **Who this article is for**: Developers and architects who want to understand the **design rationale and philosophy** behind ApexBlueprint. We read "why the API took this form" through a comparison with traditional procedural factory patterns. This page doesn't dive into the internal implementation; for that, see the sister article [Dependency Resolution Internals](/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive).

At the heart of ApexBlueprint's design is the idea of replacing **"writing down a procedure"** with **"declaring the final shape of the data"** when creating integration test data. This page contrasts the approach with **procedural test-data factory patterns in general** to dig into how "Declarative Data Specification" solves the problem from a different angle.

> The comparison target on this page is not a specific OSS library, but rather **"factory patterns in general that hold data generation logic inside methods"**. We aren't talking about whether individual TestDataFactory implementations are good or bad; we're looking at the properties of the form itself — "trapping data generation inside methods".

## Procedural Test-Data Factory Patterns

By "procedural factory" we mean a design where **record-creation logic is written inside static methods, and the caller receives completed records simply by calling those methods**. This style is widely adopted in Salesforce integration testing, and it works fine in simple cases. But broadly speaking, it falls into two typical patterns, each with its own weakness.

### Pattern A. A "Create-One-of-Each" Method per SObject

A row of methods like `createAccount()` / `createOpportunity()` / `createContact()`, one per object, each producing "one record with typical values". The caller invokes them in order and assembles relationships using the returned IDs.

```apex
@isTest
static void someTest() {
    Account acc = TestDataFactory.createAccount();
    Contact con = TestDataFactory.createContact(acc.Id);   // carry the parent Id
    Opportunity opp = TestDataFactory.createOpportunity(acc.Id);
    // ...
}
```

Weaknesses that emerge in this pattern:

- **The ID bucket relay stays on the caller side**: Receiving a parent record's ID and passing it as an argument to the child becomes a procedure the caller writes every time. As hierarchies deepen, the number of local variables for parent IDs grows until "connection code" outweighs the actual test body
- **What data gets created is invisible**: From method names, you can guess the **count level** ("one Account, one Contact, one Opportunity"), but to know what's actually happening inside (which fields are set, whether formulas run, whether required fields are satisfied), you have to follow the method definition
- **Parent-child structure is only inferable from formal ID arguments**: The relationship "the Contact hangs under the Account" can only be glimpsed from the fact that `acc.Id` was passed as an argument

### Pattern B. A Dedicated Method per Scenario

A row of methods specific to verification scenarios, like `createOppForX01WithFlagA()` / `createOppForX02WithFlagB()`. The caller invokes one line and gets a complex data structure.

```apex
@isTest
static void someTest() {
    Opportunity opp = TestDataFactory.createOppForX01WithFlagA();
    // ...
}
```

Weaknesses that emerge in this pattern:

- **Method explosion / argument explosion**: As scenario combinations grow, either new methods proliferate or existing methods' argument lists (boolean parades) expand. Once you see a call like `createOpp(true, false, true, false)`, you have no choice but to read the method definition to understand "what's being created"
- **Bloated responsibility inside the factory**: The "which scenario to build" branching accumulates inside the factory, and modifications can no longer be localized
- **High comprehension cost when revisiting later**: Reading the caller code, the method name alone doesn't tell you what's inside; you must always open the factory implementation to grasp the intent

### Pattern C. One Giant Factory for Entire Business Scenarios

A hybrid of A and B that's the most common form in the field: **"create all objects required by a business scenario inside one shared method"**. A general-purpose method like `createTestData()` internally generates Account / Opportunity / Quote / QuoteLineItem / Product all at once, and every test calls and reuses it.

```apex
@isTest
static void someTest() {
    TestDataFactory.createTestData();
    // Account / Opportunity / Quote / QuoteLineItem / Product all exist
    Opportunity opp = [SELECT Id, Amount FROM Opportunity LIMIT 1];
    // ... verify opp ...
}
```

The caller does it in a single line, and seemingly avoids both ID bucket relay and method explosion. But as scale grows, a different kind of structural weakness comes to the surface:

- **Side effects are not readable**: The test body only shows `createTestData()`, so to judge "**what does it mean for a Quote to exist alongside this test?**" or "**does the existence of a QuoteLineItem affect what's being verified?**", you ultimately have to open the factory implementation and grasp the contents of every record
- **Half-day investigations when tests fail**: It's hard to tell whether the failure is a logic problem or a trigger side effect from records created by `createTestData`
- **Pressure on governor limits from unnecessary records**: For verifying one spec, five SObjects' worth of records irrelevant to the test get created every time. As test counts grow, you start hitting governor limits
- **Collapse of test independence**: A small tweak to `createTestData` regularly causes **20 seemingly unrelated tests to fall over** in a chain
- **You still can't escape A's and B's weaknesses**: Derived methods like `createTestDataForApproval()` / `createTestDataForCancellation()` proliferate based on scenarios, and boolean argument parades creep in too. You end up with the worst-of-all situation: **both method explosion and argument explosion at once**

This pattern was introduced to escape the surface-level friction of A and B, but as the scale grows it transitions to a deeper problem: **you can no longer see the overall shape and side effects of the data from the test body**.

### The Common Root of All Three Patterns

For A, B, and C alike, the structural cause of the weakness is the same: **data generation logic is trapped inside the method**. The method's output is a completed record; the form "what field structure was used, what relationships were made" never appears in the caller's code.

On top of that, since the factory's methods themselves hold the "condition → value" generation logic, the only moves you can make as scenarios grow are "add a method or add an argument" — a structural constraint.

And here's where the **shift in how we work with code in the AI era** comes into play. As AI becomes the writer of code, the human role shifts from "**writing** code" to "understanding **what intent** the produced code was written with". Put another way, **understanding intent becomes the human's job**.

At this moment, procedural factories have a decisive structural weakness. The intent of a scenario like "an Account with three Contacts hanging under it, where only one of those Contacts has an Opportunity linked" is **hidden behind the method call (in the factory implementation), and cannot be recovered from reading the caller's test code**. A method name can be a **label for the intent**, but it can never be **the intent itself**. Whether the label is accurate or not — you can only find out by opening the factory.

What Declarative Data Specification is solving is precisely this problem of **"surfacing intent as a structure"**. If "what data should ultimately exist" is written **as the code's structure itself**, you no longer need to follow method definitions; whether the code was written by an AI or a human, the **intent can be read in the shortest path**. Beyond just lowering the cost for the reader, **the code itself doubles as documentation of intent**.

## What Declarative Data Specification Is

ApexBlueprint's response to these strains is not to "**add more APIs**" but to "**change what's being written**".

Concretely, instead of writing "the steps to create the data", you write "**the structure of the data you ultimately want to exist**" as a single expression. Mechanical work like parent Id copying, insertion order, and alias resolution is all delegated to the framework.

```apex
SOrchestrator.start()
    .add(
        SBlueprint.of(Account.class)
            .template(Blueprints.accBasic())
            .alias('acc')
            .withChildren(
                SBlueprint.of(Contact.class)
                    .set('LastName', 'Contact-{#}')
                    .times(3)
            )
            .withChildren(
                SBlueprint.of(Opportunity.class)
                    .set('Name', 'TestOpportunity')
                    .set('StageName', 'Prospecting')
                    .set('CloseDate', Date.today().addDays(30))
            )
    )
    .create();
```

Reading this code top to bottom, you immediately see the final data structure: "**one Account, with three Contacts and one Opportunity hanging under it**". No Id juggling. No insertion-order management. The code's indentation hierarchy matches the data hierarchy directly.

This is the heart of the **Declarative Data Specification** idea. The shift from "how to create data" to "**a blueprint of data**". And this shift produces a side benefit: **the code structure itself becomes documentation of intent**. "What does this test create, and what does it verify?" can be read directly from the **shape** of the test body, without opening a separate file.

### What Disappears from the Structure

Writing declaratively makes the following elements disappear from test code:

- Code that carries the parent record's Id around in a temporary variable
- Code that arranges the insertion order by thinking about it
- Code that copies values into the child's lookup
- The need to use comments to explain "what data ultimately gets created"

In their place, only the information that **the test actually needs** remains: data hierarchy, counts, and field values.

### Mechanical Work Automated Away

Behind the scenes, ApexBlueprint handles the following:

- **Dependency analysis and topological sort** for all blueprints
- Automatic determination of the **parent → child insertion order**
- **Automatic copying** of parent Ids into child lookups
- **Hierarchical resolution** of values referenced via aliases (including `{P0}` / `{P1}`)
- **Integrity checks** for circular dependencies / duplicate aliases / invalid references / ambiguous lookups

To the user these look like "everything just works once you declare the dependencies", but internally several independent problems are being solved at once.

### Templates Are "Field Presets", Not "Methods"

A notable difference from the procedural factories described above is **how shared defaults are reused**. ApexBlueprint's templates (`Blueprints.cls`) hold defaults as a **Map of field values, not as methods**.

```apex
public with sharing class Blueprints {
    public static Map<String, Object> accBasic() {
        return new Map<String, Object>{
            'Name' => 'TestAccount',
            'Industry' => 'Technology',
            'AnnualRevenue' => 500000
        };
    }
}
```

You load these via `.template(...)`, and in each test you override only the **fields that are the verification target** with `.set(...)`.

```apex
SBlueprint.of(Account.class)
    .template(Blueprints.accBasic())
    .set('Name', 'Acme Trading Co');   // only the field that matters in this test
```

Key points:

- **Templates are declarations of field combinations only and hold no logic** (no `if` branching, no conditional value generation)
- **Scenario differences are expressed not in the template but in the `.set(...)` calls in the test body**
- As a result, you basically don't need to add more methods to the template — "a few typical forms per SObject" is enough
- Reading the test body shows directly "what this test is changing and what it's verifying" from the `.set(...)` lines

In other words, ApexBlueprint steps out of the "either add methods or add arguments" dichotomy entirely, and introduces a division of labor: **"templates are where presets live; the test body is where differences are written"**.

### Relationship with the DRY Principle

After reading the above you may have the question: "**Isn't writing the final data shape directly in each test a violation of DRY?**" Similar blueprint chains showing up repeatedly across multiple tests does, on the surface, look like duplication of "code shape".

But DRY's original definition is <em>"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system"</em> (`The Pragmatic Programmer`). It is about consolidating **knowledge** in one place — not about avoiding repetition of code shape.

Looking at ApexBlueprint's design through this lens, you can see that it **deliberately separates "what should be DRY" from "what should be exposed"**:

- **Shared field combinations (= knowledge)**: consolidated into templates like `Blueprints.accBasic()` → **satisfies DRY**
- **Test-specific data hierarchy and verification intent (= that test's claim)**: written directly in the test body → **exposed as intent**

Even when similar structures appear across multiple tests, what each test verifies is a different intent — this is not duplication of knowledge but **per-intent specificity**. Applying DRY literally — "if the structures look similar, consolidate them into a shared method" — leads right back to the phenomenon we most want to avoid in the AI era: **intent hiding behind a method name**.

To put ApexBlueprint's stance in a single line:

> **DRY for knowledge; exposed for intent**

This division of labor reconciles the original spirit of DRY (a single source of knowledge) with the demand of the AI era (transparent intent). On the surface, "duplication of code shape" is tolerated — but that's a **deliberate choice to surface intent**, not a rebellion against DRY itself.

## Why This Works Especially Well in Salesforce

The idea of Declarative Data Specification is generally useful for any environment that writes integration tests. But Salesforce has structural reasons to **especially need** this approach.

### Many Fields and Complex Relationships

Salesforce's object model assumes: standard objects + custom objects + custom fields + multiple lookups + RecordTypes + required fields + validation rules. To create "one Opportunity correctly", you have to be aware of dozens of fields, and which of them are required / defaulted / validated varies from org to org.

When written down as a procedure, this complexity flows into the test body itself. Consolidating shared settings into `Blueprints.cls` and combining `.template(...) + .set(...)` is also a mechanism to **push that complexity outside the test**.

### Admin Configuration Changes the Test's Premises

In Salesforce, admins add fields, increase validation rules, and split RecordTypes — often without the developer's knowledge. To absorb these changes with a procedural factory, you have to **modify the conditional branches and value generation inside each method, one by one**. Given the premise of "a runtime environment where you don't know what changed when", procedural test data tends to fray badly.

When written declaratively, shared settings are unified in `Blueprints.cls`, so the blast radius of changes is **structurally narrowed**.

### "Remembering Everything Is Impossible" Is the Reality

In a complex business system, it's unrealistic for one developer to **keep all integration-test procedures in their head**. Whether you can re-read a factory method you wrote yourself half a year ago is genuinely doubtful.

Declarative code has the property that **"reading the code itself becomes understanding the data structure"**. This translates into a long-term benefit: lower cognitive load when your future self or a teammate intervenes in that test.

## Where It Sits in Apex Stem

[Apex Stem](/apex-stem) consists of four OSS, and ApexBlueprint plays the **integration test data generation (Test Data Factory)** role.

The [test strategy](/apex-stem/docs/test-strategy) page covers this in detail, but Apex Stem structurally separates "Usecase unit tests" and "Handler integration tests", with different OSS for each:

| Test Type | OSS in Charge | Data Generation | DML |
|---|---|---|---|
| Usecase unit test | ApexEloquent (`MockEloquent` / `MockEntry`) | In-memory mocks | None |
| Handler integration test | ApexBlueprint (`SBlueprint` / `SOrchestrator`) | Real DML records in the org | Yes |

Both address the same difficulty of "test data creation" — just in different contexts. The split: unit tests need exhaustive logic coverage (ApexEloquent's MockEntry); integration tests need "integration with real platform behavior" (ApexBlueprint's SOrchestrator).

ApexBlueprint's Declarative Data Specification is a design intended to **preserve the declarative reading experience even in integration tests**. It plays a role in supporting Apex Stem's overall consistency: "no matter the type of test, the reading experience of the test code stays the same".

## Related Documents

- [Relations, Bulk Generation, and Reference Patterns](/apex-stem/docs/apex-blueprint-relations-and-bulk): Applications of declarative form (withChildren / times / {Pn})
- [Dependency Resolution Internals: Topological Sort + Alias Resolution](/apex-stem/docs/apex-blueprint-dependency-resolution-deep-dive): A Deep Dive into what happens inside
- [Test Strategy](/apex-stem/docs/test-strategy): The division of roles between ApexBlueprint and ApexEloquent
- [Apex Stem Introduction Guide](/apex-stem/docs/apex-stem-full-guide): The entry point to the big picture
- [Back to the ApexBlueprint Guide](/apex-stem/docs/apex-blueprint-guide)
