Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 4578

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 27, 20242024-11-27T01:17:09+00:00 2024-11-27T01:17:09+00:00

Mastering DDD: Repository Design Patterns in Go

  • 61k

This article aims to deepen the understanding of “Repository”, a core concept in Domain-Driven Design (DDD). We will explore the fundamental roles and importance of a repository and present an example of its implementation in the Go language.

Prerequisites

  • Assumes an application that retrieves (and updates) data from a relational database.
  • Sample code is written in Go.

What is a Repository?

Let's start by looking at the definition of a repository.

Repository Pattern:

A repository maps the data retrieved from the database into a structure and provides an interface to access domain objects.

This aligns with the general understanding of a repository. Next, let's look at a more detailed definition in the context of DDD.

A More Detailed Definition of Repository

Check out the DDD Reference. This reference is based on Eric Evans's book on DDD, summarizing various DDD terms. On page 17, it states:

Repository Definition (DDD Reference):

Query access to aggregates expressed in the ubiquitous language.

Proliferation of traversable associations used only for finding things muddles the model. In mature models, queries often express domain concepts. Yet queries can cause problems.
The sheer technical complexity of applying most database access infrastructure quickly swamps the client code, which leads developers to dumb-down the domain layer, which makes the model irrelevant.

A query framework may encapsulate most of that technical complexity, enabling developers to pull the exact data they need from the database in a more automated or declarative way, but that only solves part of the problem.

Unconstrained queries may pull specific fields from objects, breaching encapsulation, or instantiate a few specific objects from the interior of an aggregate, blindsiding the aggregate root and making it impossible for these objects to enforce the rules of the domain model. Domain logic moves into queries and application layer code, and the entities and value objects become mere data containers.

The Role of Repository

The role of a repository is to provide access to aggregates. The aspect of “executing SQL to fill a structure with data and return it” is just a superficial facet of a repository.

A repository always exists in tandem with an aggregate. Therefore, a correct understanding of aggregates is essential for practicing DDD.

What is an Aggregate?

Let's look into the term “aggregate”. What is an aggregate? On page 16 of the DDD Reference, just above the repository section, there's an explanation of aggregates.

Definition of Aggregate (DDD Reference):

Cluster the entities and value objects into aggregates and define boundaries around each. Choose one entity to be the root of each aggregate, and allow external objects to hold references to the root only (references to internal members passed out for use within a single operation only). Define properties and invariants for the aggregate as a whole and give enforcement responsibility to the root or some designated framework mechanism.

Use the same aggregate boundaries to govern transactions and distribution.

Within an aggregate boundary, apply consistency rules synchronously. Across boundaries, handle updates asynchronously.

Keep an aggregate together on one server. Allow different aggregates to be distributed among nodes.

What is an Entity?

An entity is also defined in the DDD Reference. An entity is a domain object uniquely identifiable by an ID. In a domain model, there are entities and value objects (Value Object), the latter not having an ID.

Aggregate ≠ “Model”

The term “model” has been used in application development for a long time, but its meaning varies depending on who uses it and the context. In the past, in web application development, “model” meant the model in the MVC (Model-View-Controller) pattern.

Have you heard of Active Record? It's a design pattern adopted by Ruby on Rails and ORM libraries. In Active Record, a “model” holds data for one record of a table.

In Ruby on Rails, classes implementing Active Record are called “models” and are placed in the models/ path. Influenced by Rails, in application development, “model” usually refers to an object holding data for one row of a table.

Aggregate in DDD is different from Ruby on Rails's “model”

As mentioned, an aggregate in DDD can hold data not only for the root entity but also for other entities. Therefore, an aggregate is not a 1:1 relationship with a table but a 1:n relationship (n ≥ 1). An aggregate in DDD is not just for manipulating one record of a table.

An aggregate is a domain model. Even if composed of a single entity, please consider it a type of aggregate. A repository provides an interface to access aggregates.

Entities Composing an Aggregate

We know an aggregate comprises one or more entities. So, how should we decide which entities compose an aggregate?

The DDD Reference states:

An aggregate defines a unit that sets the attributes (properties) and invariants, responsible for maintaining these.

In other words, an aggregate is a unit that defines the attributes of a domain model and is responsible for maintaining the invariants demanded by that domain model.

Invariants are essentially relationships between data that must always be maintained. That's data consistency. To maintain data consistency (invariants), updates must occur within the same transaction. This forms the basis of an aggregate. In summary:

Entities Composing an Aggregate:
These are clusters of entities and value objects that should be treated as a unit in maintaining database data consistency (invariants). One of these entities becomes the root of the aggregate. Other aggregates can only hold references to the root entity, not the encapsulated entities within an aggregate.

Boundaries of an Aggregate

There's no silver bullet for correctly determining the boundaries of an aggregate. Properly setting aggregate boundaries requires consideration of database design, scalability, and domain knowledge. It's more challenging in complex domains, like inventory management systems.

Rule of Thumb:
Keep the boundaries of an aggregate as small as possible. This is to reduce the size of database transactions and, in turn, reduce the technical debt of the domain model.

Minimizing Aggregate Boundaries

How can we keep aggregate boundaries small? To do this, we need to consider it from the stage of table design.

The boundaries of an aggregate will become the boundaries for asynchronous processing when the service or product scales in the future. Therefore, you must decide boundaries such that data inconsistency doesn't occur even when transactions are split. Sometimes, tables need to be separated in unexpected ways.

As aggregates grow larger, the number of tables updated in synchronous transactions increases, leading to performance degradation and maintenance issues. Even if it seems that entities should be updated in one transaction, they might be separable upon closer examination. It's crucial not to be constrained by preconceptions.

Note:
When splitting transactions at aggregate boundaries, consider the implications of partial transaction failures. Ideally, a single operation in the user interface should not span multiple aggregate boundaries. Since aggregate boundaries are deeply intertwined with the user interface, it's important to align understanding with product managers and designers early in the process.

Example of a Shopping Cart

Let's consider the interface of a shopping cart for an e-commerce site. Assume there are tables for the shopping cart and cart items. Application layer use case X utilizes the data of the shopping cart and cart items (e.g., add a cart item into a shopping cart).

ER Diagram

ER diagram

Non-DDD Repository

The interface for a non-DDD approach would look like this, with a one-to-one correspondence between tables and repositories.

Class Diagram

Repository Interface:
Repositories are defined for the ShoppingCart and CartItem tables.

package repository  type ShoppingCart interface {   GetByUserID(uuid.UUID) (*model.ShoppingCart, error)   Insert(*model.ShoppingCart) error   Update(*model.ShoppingCart) error }  type CartItem interface {   GetByShoppingCartID(id uuid.UUID) ([]*CartItem, error)   Insert(*model.CartItem) error   Update(*model.CartItem) error } 
Enter fullscreen mode Exit fullscreen mode

Model Definition:

Structures representing one record of each table are defined as models. Without interfaces, domain logic cannot be encapsulated

package model  type ShoppingCart struct {   ID uuid.UUID   UserID uuid.UUID   Status model.ShoppingCartStatus }  type CartItem struct {   ID uuid.UUID   ProductID string   Quantity int } 
Enter fullscreen mode Exit fullscreen mode

Potential Issues:

This approach has the following potential issues:

  • Models become mere containers for table records.
  • Domain logic is implemented in use case X.
  • Aggregate invariants are ensured at the application layer.
  • Use case X needs to consider new and updated database operations.

DDD-Compliant Repository Example

Now, let's design a shopping cart repository following DDD principles.

The root of the aggregate is the ShoppingCart entity. CartItem entity is encapsulated within the ShoppingCart aggregate.

First, define a repository corresponding to the ShoppingCart aggregate. Unlike the previous example, there is no CartItem repository. This is because repositories are defined per aggregate, not per table. The ShoppingCart's AddItem method is an example of domain logic that is used by use case X.

Class Diagram:

Class Diagram

Repository Interface:

The repository encapsulates the process of reading and updating aggregates, so there's usually no need to separate Insert and Update. The repository takes responsibility for this in the Save method.

package domain  type ShoppingCartRepository interface {   GetByUserID(uuid.UUID) (ShoppingCart, error)   Save(ShoppingCart) error } 
Enter fullscreen mode Exit fullscreen mode

Aggregate Model Definition:

The root of the aggregate is the ShoppingCart entity. Aggregates encapsulate domain logic, so they are defined as interfaces, not structures, and are defined in the domain package. Here's the aggregate interface definition:

package domain  type ShoppingCart interface {   ID() uuid.UUID   AddItem(Product Product, Quantity int) error } 
Enter fullscreen mode Exit fullscreen mode

Improved Points:

  • The repository can fully encapsulate database complexity (insertion/update decisions).
  • No need to create a repository for each table, reducing redundant SQL coding.
  • By interfacing models, domain logic can be encapsulated.
  • Domain logic and database complexity are eliminated from the application layer.

Adopting DDD clarifies the responsibilities of domain logic, database access, and the application layer. This improvement reduces application complexity and cognitive load, enhancing development speed over time.

Repository and Aggregate Implementation

Should Aggregate and Repository Implementations be Separated?

Where should the implementations of the model and repository interfaces be placed? There's no definitive answer, but I personally believe it's preferable to place the implementations of repositories and models in the same package.

Reasons for Defining Repository and Aggregate Model Implementations in the Same Package:

The role of the repository was to encapsulate the complexity of database access, creation, and saving of aggregates. The repository needs to initialize attributes for instantiating the aggregate. Since it references and updates attributes not exposed in the interface, it needs to be defined in the same package.

For example, consider the Save method implementation of the repository. In the following implementation, the decision to register new or update in the database is encapsulated.

package shoppingcart  // Struct implementing domain.ShoppingCartRepository type repositoryImpl struct {}  // Struct implementing domain.ShoppingCart type shoppingCartImpl struct {   ID uuid.UUID   // other properties... }  func (r *repositoryImpl) Save(model domain.ShoppingCart) error {   instance := model.(*shoppingCartImpl)   if instance.ID == uuid.Nil {     // If ID is nil, it's a new registration     return r.insert(instance)   }   return r.update(instance) } 
Enter fullscreen mode Exit fullscreen mode

Next, consider the GetByUserID method implementation of the repository. The repository can return an empty shopping cart if no data exists. Regardless of whether the aggregate exists, the right instance of the aggregate is returned, simplifying the repository interface.

package shoppingcart  func (r *repositoryImpl) GetByUserID(userID uuid.UUID) (domain.ShoppingCart, error) {   data, err := r.findByUserID(userID)   if errors.Is(err, sql.ErrNotFound) {     // Return an empty shopping cart aggregate if no data exists     return newEmptyShoppingCart(), nil   }   if err != nil {     return nil, err   }   // Create an aggregate using the data of the already saved shopping cart   return newShoppngCart(data), nil } 
Enter fullscreen mode Exit fullscreen mode

Deep Module: The Importance of a Simple Interface

Deep Module is a concept explained in the book A Philosophy of Software Design by a Stanford University professor. Deep Module refers to a module that has a simple and narrow interface on the surface but contains rich functionality and complexity internally. Conversely, modules with complex interfaces but little internal functionality are called Shallow Modules. Deep Modules excel in low cognitive load, high reusability, and ease of understanding. For example, Go language's net/http package is a simple interface but encapsulates many features and complexities for implementing an HTTP server, making it easy to use. The file system of an OS is another example of a Deep Module. The concept of Deep Module has gained wide support in the IT industry in recent years.

Depth Module

Source: Depth of module

Conclusion

Implementing repositories and aggregates according to DDD principles allows encapsulation of domain logic and enhances application maintainability. Determining aggregate boundaries requires deep consideration of domain and table design, but this is arguably one of the more enjoyable aspects of software development. In future developments, I aim to actively adopt DDD to explore more enjoyable application development.

PS: Wishing everyone in Ishikawa Prefecture, Noto, and Wajima in Japan, who were affected by the earthquake on New Year's Day, a safe recovery.

dddgoprogrammingwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 1k
  • Popular
  • Answers
  • Author

    How to ensure that all the routes on my Symfony ...

    • 0 Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.