The Repository Design Pattern provides an abstraction layer between business logic and data storage, offering a consistent way to access and manage data while hiding the details of the underlying data source.
- Separates business logic from data access logic by providing a centralized and standardized approach to data operations, improving maintainability.
- Improves testability and flexibility by allowing data sources to be easily mocked, replaced, or adapted to different storage technologies.
Real-World Example
The Repository Design Pattern is like a librarian in a library.
Imagine you're at a library to find a book. You don't go into the storage room to search for it yourself, instead, you ask the librarian to help you find the book. The librarian knows where the books are kept and can give you the book you want without you having to worry about where it's stored.
- In the same way, the Repository Design Pattern works as a librarian between a program and data (like books in a library).
- Instead of the program directly looking for data, it asks for repository to find save, update, or delete the data it needs.
Implementation
Problem statement
Suppose you are developing an e-commerce application that needs to manage products. The application should be able to add new products, retrieve existing products, update product information, and delete products. Instead of directly interacting with the database, we will utilize the Repository Pattern to handle these operations.
Step 1: Define the Product Entity
The Product class defines the attributes of product, such as id, name and price. This serves as the basic data structure representing a product.
// Product entity representing a product
class Product {
public:
int id;
std::string name;
float price;
Product(int id, std::string name, float price) : id(id), name(std::move(name)), price(price) {}
};
/* Product entity representing a product */
public class Product {
public int id;
public String name;
public float price;
public Product(int id, String name, float price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class Product: # Product entity representing a product
def __init__(self, id, name, price):
self.id = id
self.name = name
self.price = price
// Product entity representing a product
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
- Defines the Product class as a blueprint for creating product objects.
- Each product has a unique id, a name, and a price.
Step 2: Create the Repository Interface
The ProductRepository class is an abstract class that declares methods to manage products, such as adding, retrieving, updating, and deleting.
// Interface for the repository
class ProductRepository {
public:
virtual void addProduct(const Product& product) = 0;
virtual Product getProductById(int productId) = 0;
virtual void updateProduct(const Product& product) = 0;
virtual void deleteProduct(int productId) = 0;
};
import java.util.List;
interface ProductRepository {
void addProduct(Product product);
Product getProductById(int productId);
void updateProduct(Product product);
void deleteProduct(int productId);
}
from abc import ABC, abstractmethod
class ProductRepository(ABC):
@abstractmethod
def add_product(self, product):
pass
@abstractmethod
def get_product_by_id(self, product_id):
pass
@abstractmethod
def update_product(self, product):
pass
@abstractmethod
def delete_product(self, product_id):
pass
class ProductRepository {
addProduct(product) {
throw new Error('Method not implemented.');
}
getProductById(productId) {
throw new Error('Method not implemented.');
}
updateProduct(product) {
throw new Error('Method not implemented.');
}
deleteProduct(productId) {
throw new Error('Method not implemented.');
}
}
- Defines a set of rules (interface) that any product repository class must follow.
- Specifies the exact methods that a repository class must implement.
Step 3: Implement a Concrete Repository
The InMemoryProductRepository class is a concrete implementation of the ProductRepository. It uses an in-memory data structure (here, a vector) to manage products.
// Concrete implementation of the repository (in-memory repository)
class InMemoryProductRepository : public ProductRepository {
private:
std::vector<Product> products;
public:
void addProduct(const Product& product) override {
products.push_back(product);
}
Product getProductById(int productId) override {
for (const auto& product : products) {
if (product.id == productId) {
return product;
}
}
return Product(-1, "Not Found", 0.0); // Return a default product if not found
}
void updateProduct(const Product& updatedProduct) override {
for (auto& product : products) {
if (product.id == updatedProduct.id) {
product = updatedProduct;
return;
}
}
}
void deleteProduct(int productId) override {
products.erase(std::remove_if(products.begin(), products.end(),
[productId](const Product& product) { return product.id == productId; }),
products.end());
}
};
import java.util.ArrayList;
import java.util.List;
class Product {
private int id;
private String name;
private double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
interface ProductRepository {
void addProduct(Product product);
Product getProductById(int productId);
void updateProduct(Product updatedProduct);
void deleteProduct(int productId);
}
class InMemoryProductRepository implements ProductRepository {
private List<Product> products = new ArrayList<>();
@Override
public void addProduct(Product product) {
products.add(product);
}
@Override
public Product getProductById(int productId) {
for (Product product : products) {
if (product.getId() == productId) {
return product;
}
}
return new Product(-1, "Not Found", 0.0); // Return a default product if not found
}
@Override
public void updateProduct(Product updatedProduct) {
for (int i = 0; i < products.size(); i++) {
if (products.get(i).getId() == updatedProduct.getId()) {
products.set(i, updatedProduct);
return;
}
}
}
@Override
public void deleteProduct(int productId) {
products.removeIf(product -> product.getId() == productId);
}
}
class Product:
def __init__(self, id, name, price):
self.id = id
self.name = name
self.price = price
class ProductRepository:
def addProduct(self, product):
pass
def getProductById(self, productId):
pass
def updateProduct(self, updatedProduct):
pass
def deleteProduct(self, productId):
pass
class InMemoryProductRepository(ProductRepository):
def __init__(self):
self.products = []
def addProduct(self, product):
self.products.append(product)
def getProductById(self, productId):
for product in self.products:
if product.id == productId:
return product
return Product(-1, "Not Found", 0.0) # Return a default product if not found
def updateProduct(self, updatedProduct):
for i, product in enumerate(self.products):
if product.id == updatedProduct.id:
self.products[i] = updatedProduct
return
def deleteProduct(self, productId):
self.products = [product for product in self.products if product.id!= productId]
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class ProductRepository {
addProduct(product) {}
getProductById(productId) {}
updateProduct(updatedProduct) {}
deleteProduct(productId) {}
}
class InMemoryProductRepository extends ProductRepository {
constructor() {
super();
this.products = [];
}
addProduct(product) {
this.products.push(product);
}
getProductById(productId) {
for (const product of this.products) {
if (product.id === productId) {
return product;
}
}
return new Product(-1, 'Not Found', 0.0); // Return a default product if not found
}
updateProduct(updatedProduct) {
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === updatedProduct.id) {
this.products[i] = updatedProduct;
return;
}
}
}
deleteProduct(productId) {
this.products = this.products.filter(product => product.id!== productId);
}
}
- Implements the methods declared in the ProductRepository interface.
- Uses an in-memory data store (vector) to add, retrieve, update, and delete products.
Step 4: Usage in Main Function
The main function demonstrates the usage of the InMemoryProductRepository by performing various operations on products.
int main() {
InMemoryProductRepository productRepo;
// Adding products
productRepo.addProduct(Product(1, "Keyboard", 25.0));
productRepo.addProduct(Product(2, "Mouse", 15.0));
// Retrieving and updating product
Product retrievedProduct = productRepo.getProductById(1);
std::cout << "Retrieved Product: " << retrievedProduct.name << " - $" << retrievedProduct.price << std::endl;
retrievedProduct.price = 30.0;
productRepo.updateProduct(retrievedProduct);
// Deleting a product
productRepo.deleteProduct(2);
return 0;
}
import java.util.ArrayList;
import java.util.List;
class Product {
int id;
String name;
double price;
public Product(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class InMemoryProductRepository {
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
}
public Product getProductById(int id) {
for (Product product : products) {
if (product.id == id) {
return product;
}
}
return null;
}
public void updateProduct(Product product) {
for (int i = 0; i < products.size(); i++) {
if (products.get(i).id == product.id) {
products.set(i, product);
break;
}
}
}
public void deleteProduct(int id) {
products.removeIf(product -> product.id == id);
}
}
public class Main {
public static void main(String[] args) {
InMemoryProductRepository productRepo = new InMemoryProductRepository();
// Adding products
productRepo.addProduct(new Product(1, "Keyboard", 25.0));
productRepo.addProduct(new Product(2, "Mouse", 15.0));
// Retrieving and updating product
Product retrievedProduct = productRepo.getProductById(1);
System.out.println("Retrieved Product: " + retrievedProduct.name + " - $" + retrievedProduct.price);
retrievedProduct.price = 30.0;
productRepo.updateProduct(retrievedProduct);
// Deleting a product
productRepo.deleteProduct(2);
}
}
class Product:
def __init__(self, id, name, price):
self.id = id
self.name = name
self.price = price
class InMemoryProductRepository:
def __init__(self):
self.products = []
def add_product(self, product):
self.products.append(product)
def get_product_by_id(self, id):
for product in self.products:
if product.id == id:
return product
return None
def update_product(self, product):
for i, p in enumerate(self.products):
if p.id == product.id:
self.products[i] = product
break
def delete_product(self, id):
self.products = [product for product in self.products if product.id!= id]
if __name__ == '__main__':
product_repo = InMemoryProductRepository()
# Adding products
product_repo.add_product(Product(1, 'Keyboard', 25.0))
product_repo.add_product(Product(2, 'Mouse', 15.0))
# Retrieving and updating product
retrieved_product = product_repo.get_product_by_id(1)
print(f'Retrieved Product: {retrieved_product.name} - ${retrieved_product.price}')
retrieved_product.price = 30.0
product_repo.update_product(retrieved_product)
# Deleting a product
product_repo.delete_product(2)
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class InMemoryProductRepository {
constructor() {
this.products = [];
}
addProduct(product) {
this.products.push(product);
}
getProductById(id) {
for (let product of this.products) {
if (product.id === id) {
return product;
}
}
return null;
}
updateProduct(product) {
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === product.id) {
this.products[i] = product;
break;
}
}
}
deleteProduct(id) {
this.products = this.products.filter(product => product.id!== id);
}
}
const productRepo = new InMemoryProductRepository();
// Adding products
productRepo.addProduct(new Product(1, 'Keyboard', 25.0));
productRepo.addProduct(new Product(2, 'Mouse', 15.0));
// Retrieving and updating product
let retrievedProduct = productRepo.getProductById(1);
console.log(`Retrieved Product: ${retrievedProduct.name} - $${retrievedProduct.price}`);
retrievedProduct.price = 30.0;
productRepo.updateProduct(retrievedProduct);
// Deleting a product
productRepo.deleteProduct(2);
- The main function acts as the entry point and demonstrates the use of the InMemoryProductRepository.
- It adds products, retrieves a product, updates its price, and deletes a product to showcase repository operations.
This code implements a simple in-memory repository for demonstration purposes, but in real-world scenario, the repository would likely interact with a database or some other persistent storage.
Advantages
The Repository Design Pattern offers several benefits that improve the structure and quality of an application.
- Improves separation of concerns by isolating data access from business logic.
- Enhances testability by allowing easy mocking of data repositories.
- Increases maintainability by centralizing data access logic.
- Provides flexibility to switch or modify data sources without impacting business code.
Disadvantages
The disadvantages of Repository Design Pattern are
- In small applications, implementing this pattern can add unnecessary complexity, making it more cumbersome than helpful.
- Adopting the repository pattern requires time to set up interfaces and repository classes, which can delay project timelines.
- Sometimes, the details of the underlying data access can leak into higher layers, reducing the effectiveness of the abstraction.
Use Cases
The use cases of Repository Design Pattern are
- Web Applications: It’s widely used in web apps to manage database interactions, making it easier to switch between different database systems.
- APIs and Services: In APIs or microservices, this pattern organizes data access and ensures consistent interactions with data.
- Large Systems: For complex systems, the repository pattern helps keep data access logic tidy, making the codebase easier to maintain.
- Testing Environments: It’s useful for creating mock repositories in testing, allowing you to simulate data access without affecting real data.
- Data Migration: When moving data between databases, the repository pattern makes transitions smoother by allowing you to swap implementations while keeping the application intact.
Limitation: Over-Abstraction of Data Sources
The Repository Pattern works well when dealing with similar types of data sources (like relational databases). However, not all data sources behave the same way.
For example:
- A database (e.g., Prisma) supports transactions and complex queries.
- An object storage system (e.g., S3) supports file uploads and pre-signed URLs.
If we try to force both into a single generic repository interface (CRUD), we may lose these specialized capabilities.