Repositories

A repository is responsible for consistently storing and retrieving a whole aggregate.
It has a simple collection-like global interface and optionally domain-specific methods.

Characteristics

Manipulates whole aggregates

A repository is responsible for storing and retrieve a whole aggregate. It manipulates the aggregate through its root. It cannot directly store or retrieve parts of the aggregate.

Illusion of in-memory collection

A repository provides the illusion of an in-memory collection of all objects that are of the corresponding aggregate root type.

Well-known interface

A repository implements a well-known interface that provides methods for adding, removing and querying objects. In the business framework this is the Repository interface.

Domain-specific methods

A repository optionally implements methods that select objects based on criteria meaningful to domain experts. Those methods return fully instantiated objects or collections of objects whose attribute values meet the criteria.

Default repository

The business framework provides a default repository for each aggregate that does not already have a generated or a custom repository. The Repository interface provides

  • CRUD methods to manipulate an aggregate,
  • Technology-agnostic querying capabilities using specifications.

Usage

To use a default repository, inject the Repository interface with the aggregate and its identifier types as generic parameters. Qualify the injection point to select an implementation. Example:

public class SomeClass {
    @Inject
    @InMemory
    private Repository<SomeAggregate, SomeAggregateId> someAggregateRepository;
    
    public void someMethod() {
        // manipulate aggregates with generic methods
    }
}

In the example above the @InMemory qualifier selects the built-in in-memory implementation. Other implementations are available in some persistence add-ons.

Generated repository

Good domain-driven design requires that repositories provides business-meaningful methods to retrieve aggregates.

In SeedStack, this can be done in a technology-agnostic way by creating an interface extending Repository. The business framework then generates an implementation of this custom interface for each persistence technology that supports it.

Declaration

Create an interface extending Repository and add custom methods to this interface. Implement them directly in the interface in a technology-agnostic manner:

public interface SomeRepository extends Repository<SomeAggregate, SomeId> {
    default Stream<SomeAggregate> objectsForCategory(String category) {
        return get(getSpecificationBuilder().of(SomeAggregate.class)
                .property("category").is(category)
                .build()
        );
    }
}

Specifications allow to write complex queries with technology-agnostic code. The business framework will automatically translate the specification into a real query and execute it.

Usage

To use the generated repository, inject the custom interface. Qualify the injection point to select an implementation.

public class SomeClass {
    @Inject
    @InMemory
    private SomeRepository someRepository;
    
    public void someMethod() {
        Stream<SomeAggregate> stream = someRepository.objectsForCategory("cat1");
    }
}

In the example above the @InMemory qualifier selects the in-memory generated implementation. Other implementations are available in some persistence add-ons.

Custom repository

Sometimes you just need to write your own technology-specific implementation for flexibility or performance reasons. In that case you have to write a custom interface extending Repository and a custom implementation.

Declaration

To create a custom repository, create an interface extending Repository:

public interface SomeRepository extends Repository<SomeAggregate, SomeId> {
    Stream<SomeAggregate> objectsForCategory(String category);
}

Then implement the interface in a class. To avoid reimplementing methods defined in the Repository interface, you must extend the base implementation provided by SeedStack:

public class SomeInMemoryRepository 
        extends BaseInMemoryRepository<SomeAggregate, SomeId> 
        implements SomeRepository {

    @Override
    public Stream<SomeAggregate> objectsByCategory(String category) {
        // implement specific query
    }
}

In the example above, the custom implementation extends the BaseInMemoryRepository base class which provides in-memory implementation for generic methods. Other base implementations to extend are available in some persistence add-ons.

Usage

To use the explicit repository, inject the custom interface:

public class SomeClass {
    @Inject
    private SomeRepository someRepository;
    
    public void someMethod() {
        Stream<SomeAggregate> stream = someRepository.objectsByCategory("category1");
        // do something with the result
    }
}

Querying by specification

Business framework repositories offer several methods that accept specifications. Querying by specification offers the ability to write complex business queries without any coupling to the underlying persistence technology.

The following methods take a specification parameter:

  • get(): returns a Stream of aggregates matching the given specification.
  • remove(): removes only the aggregates matching the given specification.
  • contains(): returns true if at least one aggregate in the repository matches the given specification.
  • count(): returns the number of aggregates matching the given specification.

For maximum efficiency, specifications are translated into technology-specific queries by SeedStack repository implementations, maintaining good performance.

Class configuration

When using default or generated repositories you have to explicitly specify the qualifier at the injection point, to choose the correct implementation.

To avoid specifying the qualifier in code, you can specify it as the defaultRepository key in class configuration:

classes:
  org:
    myorg:
      myapp:
        domain:
          model:
            someaggregate:
              defaultRepository: org.seedstack.business.util.inmemory.InMemory

The defaultRepository property expects either:

  • A qualifier annotation class name (like @InMemory),
  • Or an arbitrary string which will be used as the parameter of the @Named qualifier.

Example

Default repository

Nothing to declare but only have access to Repository methods.

Generated repository

public interface ProductRepository extends Repository<Product, ProductId> {
    default Stream<Product> discontinuedProducts() {
        return get(getSpecificationBuilder().of(Product.class)
                .property("discontinued").equalTo(true)
                .build()
        );
    }
}

Explicit repository

The repository interface:

public interface ProductRepository extends Repository<Product, ProductId> {
     Stream<Product> discontinuedProducts();    
}

And its in-memory implementation:

public class ProductJpaRepository 
        extends BaseInMemoryRepository<Product, ProductId> 
        implements ProductRepository {   
    @Override
    public Stream<Product> discontinuedProducts() {
        // in-memory implementation of the query 
    }    
}
   

On this page


Edit