4

Exploring Factories, Repositories, and Services

Factories, repositories, and services are the last major building blocks of domain-driven design (DDD) that we will learn about before bringing everything together in Part 2 of this book, where we will build some services from scratch.

None of the factories, repositories or services are unique to DDD and are often used in projects not using the DDD approach. This makes them especially important and useful to learn about, as you will see them everywhere.

In this chapter, we will cover the following topics:

  • The factory pattern – we will discuss what it is and when it is useful
  • The repository pattern – we will walk through some examples to help you understand how they differ from database tables
  • Services – we will look at domain services, application services, and infrastructure services and the difference between them all

By the end of this chapter, you will be able to understand factories, repositories, and services in the context of DDD as we explore these topics with the help of examples.

Technical requirements

In this chapter, we will write a small amount of Golang code. To be able to run it, you will need the following:

Introducing the factory pattern

The factory pattern is typically used in object-oriented programming and is defined as an object with the primary responsibility of creating other objects. An example from PHP might look like the following:

class Factory
{
    public static function build($carType)
    {
        if ($carType == "tesla") {
            return new Tesla();
        }
        if ($carType == "bmw") {
            return new BMW();
        }
    }
}
$myCar = Factory::build("tesla");

The factory class has a static method that accepts carType and returns a new instance. This is a very simple example, but we could also extend it to set sensible default properties on our car object. Typically, factory classes should have no other purpose than object creation.

While Golang is not an object-oriented language, the factory pattern is still useful. Here is the same example we discussed earlier, but this time in Golang:

package chapter4
import (
   "errors"
   "log"
)
type Car interface {
   BeepBeep()
}
type BMW struct {
   heatedSeatSubscriptionEnabled bool
}
func (B BMW) BeepBeep() {
   //TODO implement me
   panic("implement me")
}
type Tesla struct {
   autoPilotEnabled bool
}
func (t Tesla) BeepBeep() {
   //TODO implement me
   panic("implement me")
}
func BuildCar(carType string) (Car, error) {
   switch carType {
   case "bmw":
      return BMW{heatedSeatSubscriptionEnabled: true}, nil
   case "tesla":
      return Tesla{autoPilotEnabled: true}, nil
   default:
      return nil, errors.New("unknown car type")
   }
}
func main() {
   myCar, err := BuildCar("tesla")
   if err != nil {
      log.Fatal(err)
   }
   // do something with myCar
}

In this example, we have created a very simple function that initializes some fields for us, making it very easy for the caller of BuildCar to use. We also have returned error if the car type is not valid.

Factories are a great way to standardize the creation of complex structs and can be useful as your application grows in complexity. Factories also provide encapsulation (that is, hiding the internal details of an object from the caller and only exposing the minimal interface they need). Finally, factories can help ensure business invariants are enforced at the time of object creation, which can dramatically simplify our domain model.

For example, if we were creating a booking system for a hair salon, and someone tried to create a booking outside of business hours, we might enforce this in our CreateBooking factory function as follows:

package chapter4
import (
   "errors"
   "time"
   "github.com/google/uuid"
)
type Booking struct {
   id            uuid.UUID
   from          time.Time
   to            time.Time
   hairDresserID uuid.UUID
}
func CreateBooking(from, to time.Time, hairDresserID uuid.UUID) (*Booking, error) {
   closingTime, _ := time.Parse(time.Kitchen, "17:00pm")
   if from.After(closingTime) {
      return nil, errors.New("no appointments after closing time")
   }
   return &Booking{
      hairDresserID: uuid.New(),
      id:            uuid.New(),
      from:          from,
      to:            to,
   }, nil
}

The preceding example shows a factory function creating an entity. I have chosen to let the factory generate the ID. Let’s explore entity factories a little bit more.

Entity factories

As we discussed in the previous chapter, entities have identities and they have a minimum set of requirements necessary to instantiate them. We should, therefore, ensure we create entities that satisfy this minimum set of requirements when we create them via a factory. If we want to set other properties, we can then provide other functions.

When designing an entity factory function, we need to decide whether we want the factory function to be responsible for generating the identity for our struct or whether we want to pass one as a parameter. Both ways are fine, but I tend to lean toward letting the factory function generate it unless you have a good reason not to.

Now we understand entity factories, let’s look at repositories.

Implementing the repository pattern in Golang

Repositories are the parts of our code that contain the logic necessary to access data sources. A data source can be a wide variety of things, such as a file on disk, a spreadsheet, or an AWS S3 bucket, but in most projects, it is a database.

By using a repository layer, you can centralize common data access code and make your system more maintainable by decoupling from a specific database technology. For example, your company may have a desire to move from one cloud provider to another, and the database options are slightly different; perhaps one has a MySQL offering, and the other offers only the NoSQL databases. In this instance, we know we only need to rearchitect a small portion of our system (the repository layer) to be able to enable this change.

Some developers query the database using other channels (such as Command and Query Responsibility Segregation (CQRS), which we will discuss in Part 2). This can work, since queries should not change the state of the database, but if you are just starting, ensuring that all interactions with the database happen in the repository layer is recommended.

One mistake we often make with repository layers is to make one struct per database table. This should be avoided; instead, aim to make one struct per aggregate. The following diagram should help outline this:

Figure 4.1 – How to think about repository layers and how other layers interact with them

Figure 4.1 – How to think about repository layers and how other layers interact with them

Figure 4.1 shows a clear distinction between our database tables and our repository layer; a repository layer can write to multiple tables. Furthermore, our domain layer is decoupled from our repository layer. When we use DDD, we should always strive to build a system that looks like the one shown in Figure 4.1.

Let’s continue with the booking system example from the previous section. We want to save our hair booking appointment to a database. We might define our interface as follows:

type BookingRepository interface {
   SaveBooking(ctx context.Context, booking Booking) error
   DeleteBooking(ctx context.Context, booking Booking) error
}

We define this interface in the same package as our Booking factory and our service layer (there will be more about services in the next section).

An implementation of a simple repository layer for a Postgres database might look like this:

type PostgresRepository struct {
   connPool *pgx.Conn
}
func NewPostgresRepository(ctx context.Context, dbConnString string) (*PostgresRepository, error) {
   conn, err := pgx.Connect(ctx, dbConnString)
   if err != nil {
      return nil, fmt.Errorf("failed to connect to db: %w", err)
   }
   defer conn.Close(ctx)
   return &PostgresRepository{connPool: conn}, nil
}
func (p PostgresRepository) SaveBooking(ctx context.Context, booking Booking) error {
   _, err := p.connPool.Exec(
      ctx,
      "INSERT into bookings (id, from, to, hair_dresser_id) VALUES ($1,$2,$3,$4)",
      booking.id.String(),
      booking.from.String(),
      booking.to.String(),
      booking.hairDresserID.String(),
   )
   if err != nil {
      return fmt.Errorf("failed to SaveBooking: %w", err)
   }
   return nil
}
func (p PostgresRepository) DeleteBooking(ctx context.Context, booking Booking) error {
   _, err := p.connPool.Exec(
      ctx,
      "DELETE from bookings WHERE id = $1",
      booking.id,
   )
   if err != nil {
      return fmt.Errorf("failed to DeleteBooking: %w", err)
   }
   return nil
}

As you can see, the interaction with the database is very simple, and there is no domain logic here; we would expect that to happen in the application service layer. Next, let’s look at services and application services.

Understanding services

In DDD, we use a few different types of services to help us organize our code. These are application services, domain services, and infrastructure services. In this section, we will discuss all three services and when they are useful, starting with the domain service.

Domain services

Domain services are stateless operations within a domain that complete a certain activity. Sometimes, we will come across processes we cannot find a good way to model in an entity or value object; in these cases, it’s a good idea to use a domain service.

It is particularly tricky to outline rules to use domain services; however, some things that you should look out for are the following:

  • The code you are about to write performs a significant piece of business logic within one domain
  • You are transforming one domain object into another
  • You are taking the properties of two or more domain objects to calculate a value

Services should always be expressed using ubiquitous language from within the bounded context, just like everything else we do in DDD.

Let’s look at a couple of examples of where a service can be helpful. Imagine we have the following pieces of code within our entities:

package chapter4
type Product struct {
   ID             int
   InStock        bool
   InSomeonesCart bool
}
func (p *Product) CanBeBought() bool {
   return p.InStock && !p.InSomeonesCart
}
type ShoppingCart struct {
   ID          int
   Products    []Product
   IsFull      bool
   MaxCartSize int
}
func (s *ShoppingCart) AddToCart(p Product) bool {
   if s.IsFull {
      return false
   }
   if p.CanBeBought() {
      s.Products = append(s.Products, p)
      return true
   }
   if s.MaxCartSize == len(s.Products) {
      s.IsFull = true
   }
   return true
}

The code looks reasonable at first, but it is problematic. While implementing ShoppingCart, we referenced another entity and added business logic, which does not really belong to ShoppingCart. To avoid this issue, we move the logic to a domain service, as follows:

package chapter4
import "errors"
type CheckoutService struct {
   shoppingCart *ShoppingCart
}
func NewCheckoutService(shoppingCart *ShoppingCart) *CheckoutService {
   return &CheckoutService{shoppingCart: shoppingCart}
}
func (c CheckoutService) AddProductToBasket(p *Product) error {
   if c.shoppingCart.IsFull {
      return errors.New("cannot add to cart, its full")
   }
   if p.CanBeBought() {
      c.shoppingCart.Products = append(c.shoppingCart.Products, *p)
      return nil
   }
   if c.shoppingCart.MaxCartSize == len(c.shoppingCart.Products) {
      c.shoppingCart.IsFull = true
   }
   return nil
}

We now have a central place to house domain logic that spans two entities. This will become even more useful as we add more logic to CheckoutService that may use more entities (perhaps a discount entity or a shipping entity). Having this logic in a single-domain service means that if other clients want to implement our behavior, they can use our service, and our business invariants will be automatically enforced.

Domain services are perfect for when we need to compose domain logic in a stateless manner. However, if this doesn’t fit our use case, we likely need an application service.

Application services

Application services are used to compose other services and repositories. They are responsible for managing transactional guarantees in place among various models. They should not contain domain logic (this belongs in the domain service, as discussed in the previous section).

Application services are usually very thin. They are used only for coordination, and all the other logic should be pushed down into the layers underneath the application layer. Typically, we also address security concerns in this layer.

An example in our booking context might look as follows:

package chapter4
import (
   "context"
   "errors"
   "fmt"
   "github.com/PacktPublishing/Domain-Driven-Design-with-GoLang/chapter2"
)
type accountKey = int
const accountCtxKey = accountKey(1)
type BookingDomainService interface {
   CreateBooking(ctx context.Context, booking Booking) error
}
type BookingAppService struct {
   bookingRepo          BookingRepository
   bookingDomainService BookingDomainService
}
func NewBookingAppService(bookingRepo BookingRepository, bookingDomainService BookingDomainService) *BookingAppService {
   return &BookingAppService{bookingRepo: bookingRepo, bookingDomainService: bookingDomainService}
}
func (b *BookingAppService) CreateBooking(ctx context.Context, booking Booking) error {
   u, ok := ctx.Value(accountCtxKey).(*chapter2.Customer)
   if !ok {
      return errors.New("invalid customer")
   }
   if u.UserID() != booking.userID.String() {
      return errors.New("cannot create booking for other users")
   }
   if err := b.bookingDomainService.CreateBooking(ctx, booking); err != nil {
      return fmt.Errorf("could not create booking: %w", err)
   }
   if err := b.bookingRepo.SaveBooking(ctx, booking); err != nil {
      return fmt.Errorf("could not save booking: %w", err)
   }
   return nil
}

As you can see, we do some basic authorization checks and then compose our domain layer with our repository layer. In this specific instance, it would have been fine for our domain service to do the persistence too (since we do not cross any domain boundaries). By the end of this code block, we will have created and saved a new booking.

One other use case for application services is to power a user interface (UI). UIs may need to compose many different domain services, as demonstrated by the following flowchart. An application service can help us achieve this too.

Figure 4.2 –  A UI making use of application service

Figure 4.2 – A UI making use of application service

Figure 4.2 shows how a UI might use an application service to compose multiple different domain services to show a single screen to a user, with all the information.

Most modern web applications do the following:

  • Accept payment (perhaps using Stripe or PayPal)
  • Send email (perhaps using Amazon SES or Mailchimp)
  • Track user behavior (perhaps using Mixpanel or Google Analytics)

None of these functions are part of our primary domain, but we still want to include them in our application. To do this, we can use an infrastructure service. This can then be added to your application service or domain service.

An implementation of an email infrastructure service might look as follows:

package chapter4
import (
   "bytes"
   "context"
   "encoding/json"
   "fmt"
   "net/http"
)
type EmailSender interface {
   SendEmail(ctx context.Context, to string, title string, body string) error
}
const emailURL = "https://mandrillapp.com/api/1.0/messages/send""
type MailChimp struct {
   apiKey     string
   from       string
   httpClient http.Client
}
type MailChimpReqBody struct {
   Key     string `json:"key"`
   Message struct {
      FromEmail string `json:"from_email"`
      Subject   string `json:"subject"`
      Text      string `json:"text"`
      To        []struct {
         Email string `json:"email"`
         Type  string `json:"type"`
      } `json:"to"`
   } `json:"message"`
}
func NewMailChimp(apiKey string, from string, httpClient http.Client) *MailChimp {
   return &MailChimp{apiKey: apiKey, from: from, httpClient: httpClient}
}
func (m MailChimp) SendEmail(ctx context.Context, to string, title string, body string) error {
   bod := MailChimpReqBody{
      Key: m.apiKey,
      Message: struct {
         FromEmail string `json:"from_email"`
         Subject   string `json:"subject"`
         Text      string `json:"text"`
         To        []struct {
            Email string `json:"email"`
            Type  string `json:"type"`
         } `json:"to"`
      }{
         FromEmail: m.from,
         Subject:   title,
         Text:      body,
         To: []struct {
            Email string `json:"email"`
            Type  string `json:"type"`
         }{{Email: to, Type: "to"}},
      },
   }
   b, err := json.Marshal(bod)
   if err != nil {
      return fmt.Errorf("failed to marshall body: %w", err)
   }
   req, err := http.NewRequest(http.MethodPost, emailURL, bytes.NewReader(b))
   if err != nil {
      return fmt.Errorf("failed to create request: %w", err)
   }
   if _, err := m.httpClient.Do(req); err != nil {
      return fmt.Errorf("failed to send email: %w", err)
   }
   return nil
}

We could then add it to our application service:

type BookingAppService struct {
   bookingRepo          BookingRepository
   bookingDomainService BookingDomainService
   emailService         EmailSender
}
…

Then, we could define a CreateBooking function as follows:

func (b *BookingAppService) CreateBooking(ctx context.Context, booking Booking) error {
   u, ok := ctx.Value(accountCtxKey).(*chapter2.Customer)
   if !ok {
      return errors.New("invalid customer")
   }
   if u.UserID() != booking.userID.String() {
      return errors.New("cannot create booking for other users")
   }
   if err := b.bookingDomainService.CreateBooking(ctx, booking); err != nil {
      return fmt.Errorf("could not create booking: %w", err)
   }
   if err := b.bookingRepo.SaveBooking(ctx, booking); err != nil {
      return fmt.Errorf("could not save booking: %w", err)
   }
   err := b.emailService.SendEmail(ctx, ...)
   if err != nil {
    // handle it.
}
   return nil
}
…

As you can see, by the end of this code block, we have done the following:

  • Created a booking
  • Saved it to our database
  • Sent an email to our customers to notify them about it

Summary

In this chapter, we learned about three different service types – application, domain, and infrastructure – and we saw some examples of what they might look like. We also learned about repository layers and their benefits. Finally, we looked at how we can use factories to simplify object creation as our application gets more complex.

This wraps up Part 1 of this book. By now, you should have a preliminary understanding of all the concepts you need to implement a service using DDD. In Part 2 of this book, we will put our new knowledge to good use as we build an entire service from scratch, using everything we have learned so far and a couple of new topics wherever relevant.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.105.124