In software engineering, the repository design pattern is a layer between the business logic and the data access logic. It provides an abstract interface for managing the storage of data, such as saving and retrieving data from a database.
The repository design pattern is used to decouple the business logic of an application from the data access logic. This allows the business logic to be more reusable and testable, as it is not tied to a specific data storage implementation. It also makes it easier to switch between different data storage options, such as a relational database, a NoSQL database, a file system, a remote API, or even in-memory data structures.
The repository design pattern typically consists of a repository interface and one or more concrete implementations of the repository. The interface defines the methods that can be used to manipulate the data, such as saving, updating, and deleting data. The concrete implementations of the repository handle the details of storing and retrieving the data from the underlying data store.
The repository design pattern is often used in conjunction with the unit of work design pattern, which manages the transactions and state of the data in the repository.
Here is an example of a repository interface and a concrete implementation in Go:
// Repository is the interface for a user repository.
typeRepositoryinterface{Get(idint)(*User,error)Save(user*User)errorUpdate(user*User)errorDelete(idint)error}// UserRepository is a concrete implementation of a user repository.
typeUserRepositorystruct{db*sql.DB}// Get retrieves a user from the repository by ID.
func(r*UserRepository)Get(idint)(*User,error){// Query the database for the user with the given ID.
row:=r.db.QueryRow("SELECT * FROM users WHERE id = $1",id)// Scan the row into a user struct.
varuserUsererr:=row.Scan(&user.ID,&user.Name,&user.Email)iferr!=nil{iferr==sql.ErrNoRows{returnnil,fmt.Errorf("user not found")}returnnil,fmt.Errorf("failed to get user: %w",err)}return&user,nil}// Save saves a new user to the repository.
func(r*UserRepository)Save(user*User)error{// Insert the user into the database.
_,err:=r.db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)",user.Name,user.Email)iferr!=nil{returnfmt.Errorf("failed to save user: %w",err)}returnnil}// Update updates an existing user in the repository.
func(r*UserRepository)Update(user*User)error{// Update the user in the database.
_,err:=r.db.Exec("UPDATE users SET name = $1, email = $2 WHERE id = $3",user.Name,user.Email,user.ID)iferr!=nil{returnfmt.Errorf("failed to update user: %w",err)}returnnil}// Delete removes a user from the repository.
func(r*UserRepository)Delete(idint)error{// Delete the user from the database.
_,err:=r.db.Exec("DELETE FROM users WHERE id = $1",id)iferr!=nil{returnfmt.Errorf("failed to delete user: %w",err)}returnnil}
To use the repository, the business logic of the application can depend on the Repository interface and use it to manipulate the data without having to worry about the details of how the data is stored. For example:
1
2
3
4
5
6
7
8
funcUpdateUser(repoRepository,user*User)error{// Update the user in the repository.
iferr:=repo.Update(user);err!=nil{returnfmt.Errorf("failed to update user: %w",err)}returnnil}
To use the repository in the client code, you will need to instantiate a concrete implementation of the repository and inject it into the client code. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
funcmain(){// Connect to the database.
db,err:=sql.Open("postgres","user=postgres password=mypassword dbname=mydb sslmode=disable")iferr!=nil{log.Fatal(err)}deferdb.Close()// Create a new user repository.
repo:=&UserRepository{db:db}// Use the repository to update a user.
user:=&User{ID:1,Name:"John",Email:"[email protected]"}iferr:=UpdateUser(repo,user);err!=nil{log.Fatal(err)}}
In this example, the client code connects to a database and creates a new UserRepository instance, passing in the database connection as a dependency. It then calls the UpdateUser function and passes in the repo instance as the repository. The UpdateUser function can then use the Repository interface to update the user in the repository without having to worry about the details of how the data is stored.
The repository design pattern can be applied to any type of data storage or retrieval system, not just databases.
For example, suppose you are building a client for a REST API that provides access to a collection of resources. You could implement a repository interface that defines methods for retrieving, creating, updating, and deleting resources from the API, and a concrete implementation that handles the details of making HTTP requests to the API and parsing the responses. The business logic of your application could then depend on the repository interface and use it to manipulate the resources without having to worry about the details of how the data is being stored or retrieved.
// Repository is the interface for a resource repository.
typeRepositoryinterface{Get(idstring)(*Resource,error)GetAll()([]*Resource,error)Save(resource*Resource)(*Resource,error)Update(resource*Resource)(*Resource,error)Delete(idstring)error}// ResourceRepository is a concrete implementation of a resource repository.
typeResourceRepositorystruct{apiClient*http.ClientbaseURLstring}// Get retrieves a resource from the repository by ID.
func(r*ResourceRepository)Get(idstring)(*Resource,error){// Make a GET request to the API to retrieve the resource.
req,err:=http.NewRequest("GET",r.baseURL+"/resources/"+id,nil)iferr!=nil{returnnil,fmt.Errorf("failed to create request: %w",err)}resp,err:=r.apiClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("failed to get resource: %w",err)}deferresp.Body.Close()// Parse the response body into a resource struct.
varresourceResourceiferr:=json.NewDecoder(resp.Body).Decode(&resource);err!=nil{returnnil,fmt.Errorf("failed to parse response: %w",err)}return&resource,nil}// GetAll retrieves all resources from the repository.
func(r*ResourceRepository)GetAll()([]*Resource,error){// Make a GET request to the API to retrieve all resources.
req,err:=http.NewRequest("GET",r.baseURL+"/resources",nil)iferr!=nil{returnnil,fmt.Errorf("failed to create request: %w",err)}resp,err:=r.apiClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("failed to get resources: %w",err)}deferresp.Body.Close()// Parse the response body into a slice of resource structs.
varresources[]*Resourceiferr:=json.NewDecoder(resp.Body).Decode(&resources);err!=nil{returnnil,fmt.Errorf("failed to parse response: %w",err)}returnresources,nil}// Save creates a new resource in the repository.
func(r*ResourceRepository)Save(resource*Resource)(*Resource,error){// Marshal the resource into a JSON payload.
payload,err:=json.Marshal(resource)iferr!=nil{returnnil,fmt.Errorf("failed to marshal resource: %w",err)}// Make a POST request to the API to create the resource.
req,err:=http.NewRequest("POST",r.baseURL+"/resources",bytes.NewReader(payload))iferr!=nil{returnnil,fmt.Errorf("failed to create request: %w",err)}resp,err:=r.apiClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("failed to create resource: %w",err)}deferresp.Body.Close()// Parse the response body into a resource struct.
varcreatedResourceResourceiferr:=json.NewDecoder(resp.Body).Decode(&createdResource);err!=nil{returnnil,fmt.Errorf("failed to parse response: %w",err)}return&createdResource,nil}// Update updates an existing resource in the repository.
func(r*ResourceRepository)Update(resource*Resource)(*Resource,error){// Marshal the resource into a JSON payload.
payload,err:=json.Marshal(resource)iferr!=nil{returnnil,fmt.Errorf("failed to marshal resource: %w",err)}// Make a PUT request to the API to update the resource.
req,err:=http.NewRequest("PUT",r.baseURL+"/resources/"+resource.ID,bytes.NewReader(payload))iferr!=nil{returnnil,fmt.Errorf("failed to create request: %w",err)}resp,err:=r.apiClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("failed to update resource: %w",err)}deferresp.Body.Close()// Parse the response body into a resource struct.
varupdatedResourceResourceiferr:=json.NewDecoder(resp.Body).Decode(&updatedResource);err!=nil{returnnil,fmt.Errorf("failed to parse response: %w",err)}return&updatedResource,nil}// Delete removes a resource from the repository.
func(r*ResourceRepository)Delete(idstring)error{// Make a DELETE request to the API to delete the resource.
req,err:=http.NewRequest("DELETE",r.baseURL+"/resources/"+id,nil)iferr!=nil{returnfmt.Errorf("failed to create request: %w",err)}resp,err:=r.apiClient.Do(req)iferr!=nil{returnfmt.Errorf("failed to delete resource: %w",err)}deferresp.Body.Close()returnnil}
The client code can instantiate a concrete implementation of the repository and inject it into the function when it is called, like this:
funcmain(){// Create a new HTTP client.
client:=&http.Client{}// Create a new resource repository.
repo:=&ResourceRepository{apiClient:client,baseURL:"https://api.example.com"}// Use the repository to retrieve a resource by ID.
resource,err:=repo.Get("1")iferr!=nil{log.Fatal(err)}fmt.Println(resource)// Use the repository to retrieve all resources.
resources,err:=repo.GetAll()iferr!=nil{log.Fatal(err)}fmt.Println(resources)// Use the repository to save a new resource.
newResource:=&Resource{Name:"New Resource",Description:"This is a new resource"}savedResource,err:=repo.Save(newResource)iferr!=nil{log.Fatal(err)}fmt.Println(savedResource)// Use the repository to update the new resource.
savedResource.Description="This resource has been updated"updatedResource,err:=repo.Update(savedResource)iferr!=nil{log.Fatal(err)}fmt.Println(updatedResource)// Use the repository to delete the new resource.
iferr:=repo.Delete(savedResource.ID);err!=nil{log.Fatal(err)}}
In this example, the main function creates a new ResourceRepository and uses it to retrieve a resource by ID, retrieve all resources, save a new resource, update the new resource, and delete the new resource. The repository handles the details of making the appropriate HTTP requests to the API and parsing the responses, allowing the business logic to focus on manipulating the resources.