Repository pattern: separate data access

A service asks an interface for data. The repository hides where that data comes from, so the service does not depend on a database or a list.

Time depends on storageSpace depends on storageCode C++, Python and C#
Watch it animatedStep through it, with the running line highlighted. Free with an account.

The idea

The service depends on IProductRepository, a contract for the data it needs. ListProductRepository is one implementation; a database-backed repository can replace it without changing ProductService. The visual follows one request through the service, interface, and implementation.

Code

struct Product { int id; string name; };

class ProductRepository {
public:
    virtual optional<Product> getById(int id) = 0;
    virtual ~ProductRepository() = default;
};
class ListProductRepository : public ProductRepository {
    vector<Product> products;
public:
    ListProductRepository(vector<Product> products) : products(move(products)) {}
    optional<Product> getById(int id) override {
        for (auto& p : products) if (p.id == id) return p;
        return nullopt;
    }
};
class ProductService {
    ProductRepository& repository;
public:
    ProductService(ProductRepository& repository) : repository(repository) {}
    string nameOf(int id) { auto p = repository.getById(id); return p ? p->name : "missing"; }
};

int main() {
    ListProductRepository repository({{7, "Book"}, {8, "Pen"}});
    ProductService service(repository);
    cout << service.nameOf(7) << "\n";
}
from dataclasses import dataclass
from typing import Protocol

@dataclass(frozen=True)
class Product:
    id: int
    name: str

class ProductRepository(Protocol):
    def get_by_id(self, id: int) -> Product | None: ...

class ListProductRepository:
    def __init__(self, products: list[Product]):
        self.products = products
    def get_by_id(self, id: int) -> Product | None:
        return next((p for p in self.products if p.id == id), None)

class ProductService:
    def __init__(self, repository: ProductRepository):
        self.repository = repository
    def name_of(self, id: int) -> str:
        p = self.repository.get_by_id(id); return p.name if p else "missing"

repository = ListProductRepository([Product(7, "Book"), Product(8, "Pen")])
service = ProductService(repository)
print(service.name_of(7))
record Product(int Id, string Name);

interface IProductRepository {
    Product? GetById(int id);
}
class ListProductRepository : IProductRepository {
    private readonly List<Product> products;
    public ListProductRepository(List<Product> products) { this.products = products; }
    public Product? GetById(int id) => products.FirstOrDefault(p => p.Id == id);
}
class ProductService {
    private readonly IProductRepository repository;
    public ProductService(IProductRepository repository) { this.repository = repository; }
    public string NameOf(int id) => repository.GetById(id)?.Name ?? "missing";
}

var repository = new ListProductRepository(
    new List<Product> { new(7, "Book"), new(8, "Pen") });
var service = new ProductService(repository);
Console.WriteLine(service.NameOf(7));

Tips

In the .NET library

In dependency injection, register IProductRepository with the implementation to use. Tests can provide a small fake repository with predictable data.