Repository Pattern Challenges in Apex and the Built-in Repository Solution

Apex Stem Docs
ApexRepository PatternArchitectureApexEloquent
Explore the unique challenges of implementing Repository patterns in Salesforce Apex and discover how Built-in Repository with Query Delegation Pattern provides a practical, scalable solution for modern Apex development.

While there's abundant information about the Repository pattern on the internet with various implementation examples, applying these implementations to Apex presents several unique challenges.

Here, I'll introduce the Repository class configuration patterns I've actually tried in Apex, along with their respective advantages and disadvantages.

Pattern 1: Use Case-Based Repository

This style defines individual Repository classes for each use case, where processing classes exist per use case. While this approach appears flexible in design, it creates the challenge of mass generation of interfaces and concrete classes.

Since Apex does not support namespaces, creating a large number of classes can easily lead to name collisions and naming exhaustion, which is a design constraint.

Pattern 2: Object-Based Repository (Selector Pattern)

To reduce class count, there's also the approach of defining Repository (Selector) per SObject unit. Salesforce's official "Selector Pattern" introduces this style.

📚 Official Selector Pattern Guide (Trailhead)

However, for frequently used objects like Opportunity with multiple purposes, the following problems arise:

  • Proliferation of detailed retrieval methods to handle conditional branching
  • Increased query control through flag arguments, making caller intentions unclear
  • Method context becomes ambiguous, making safe modifications impossible

I consider this situation a classic example of "the road to debt paved with good intentions".

Solution: Built-in Repository and Query Delegation Pattern

Based on these challenges, I propose the Query Delegation Pattern.

This pattern adopts a design where query construction is performed in the domain layer, delegating only pure I/O processing to the Repository. This allows having just one common "Built-in Repository" class.

Advantages of This Pattern

  • ✅ Prevents excessive proliferation of Repository classes
  • ✅ Keeps the intention "I want to retrieve this kind of data" and query construction in the same place
  • ✅ Can absorb detailed query differences in the domain layer, avoiding Repository bloat
  • ✅ No need to define Interface, making tests dramatically easier to write

In fact, this pattern in my projects has significantly lowered the psychological barrier to writing test code, improving productivity.

A Key Principle: Immutability

A core architectural principle of the query builder (Scribe) is its immutability.

Every time you add a condition or a field (e.g., using .whereEqual() or .field()), it doesn't modify the original query object. Instead, it returns a new, modified instance. This design offers two powerful advantages:

  • Safety: It completely prevents side effects. A base query can be passed around your application without any risk of it being accidentally modified.
  • Reusability: It allows you to create a base query and then safely "branch" it into multiple, more specific queries

Example:

APEX
// Create a base query for all open opportunities
Scribe baseQuery = Scribe.source(Opportunity.getSObjectType())
    .whereEqual('IsClosed', false);
 
// Safely create variations without modifying the baseQuery
Scribe highValueQuery = baseQuery.whereGreaterThan('Amount', 100000);
Scribe urgentQuery = baseQuery.whereDate('CloseDate', 'THIS_MONTH');

Key Features of Eloquent and MockEloquent

Eloquent and its test-double counterpart, MockEloquent, are core components of ApexEloquent. They provide the following features:

Data Retrieval

  • get() and first(): Retrieve query results as IEntry objects, which act as wrappers for the underlying SObject records.

DML Operations

  • doInsert(), doUpdate(), doDelete(): Perform standard DML operations for single SObjects or List.

Complete Mocking Support

  • MockEloquent: Provides a complete, in-memory mock of all Eloquent methods, enabling true, database-free unit testing.
  • Automatic Fake ID Assignment: The mocked doInsert() method automatically assigns a realistic, fake record ID, simulating real database behavior.

Advanced Testing Features

  • Mocking Non-writable Fields: Override values for any field in your tests—even formula fields and other read-only system fields—using the powerful MockEntry.
  • "Select-Forgotten" Detection: Get immediate feedback in your unit tests. The mock framework throws an error if you try to access a field that wasn't part of your Scribe query, ensuring your tests and production code behave identically.

These features significantly reduce data dependency issues and enhance safety in unit testing.

Comparison with Traditional Patterns

AspectTraditional RepositoryBuilt-in Repository
Class CountHigh (multiple per use case)Low (single shared class)
Test ComplexityComplex mocking setupSimple mock configuration
Query LocationScattered across repositoriesCentralized in domain layer
MaintenanceDifficult with many interfacesEasy with single interface
Apex CompatibilityPoor (namespace issues)Excellent (Apex-optimized)

Conclusion

The "Query Delegation Pattern" combined with a "Built-in Repository" is more than just a practical choice—it's a paradigm shift for Apex development.

This approach liberates developers from the pitfalls of manual SOQL and database-dependent tests. It empowers teams to adopt a modern, test-driven workflow that was previously difficult to achieve on the Salesforce platform. By embracing this model, your team can not only reduce technical debt and accelerate development but also foster a culture of writing cleaner, more reliable, and fundamentally more maintainable code.