ga('send', 'pageview');
Categories
Teknik

Domain Driven Design in Go: Part 3

Up until now, we have only looked at one service in isolation, but this is seldom the case in a service-oriented architecture. For the last post in this blog series on Domain Driven Design in Go we will have a look at how we interact with other services. In particular, we will have a look at two concepts that help us reason about these interactions: application services and bounded contexts.

Up until now, we have only looked at one service in isolation, but this is seldom the case in a service-oriented architecture. For the last post in this blog series on Domain Driven Design in Go we will have a look at how we interact with other services. In particular, we will have a look at two concepts that help us reason about these interactions: application services and bounded contexts.

 

As this project is intended to fit into a service-oriented architecture, we will also need to address some of the challenges of distributed systems if this is going to be a realistic real-world example. There are a couple of projects available that will help you deal with these issues. I decided to go with Go kit, a popular tool kit for building microservices that “solve common problems in distributed systems, so you can focus on your business logic” (from the description on Github). Generally though, there should be nothing preventing you from going with other libraries/frameworks like Gizmo, mercury, or Kite.

Application services

In the sample application, domain packages like cargo  and voyage  contain important business logic that collectively represent the domain model in our application. They define the core concepts in our business and what invariants we care about. Most likely though, we do not want to expose the entire domain model to our consumers. Instead, we try to identify the core use cases that our users care about. These use cases are exposed through application services that will define the operations that are available to our consumers. In the goddd application, there are three application services that each provide a set of operations, where each operation uses concepts from the domain model in order to fulfill a specific use case, e.g. tracking a cargo.

Go kit makes this really simple. Here is one of the application services, namely the one that allows us to track a cargo:

// package tracking

type Service interface {
    Track(id string) (Cargo, error)
}

type service struct {
    cargos cargo.Repository
}

func (s *service) Track(id string) (Cargo, error) {
    // ...
}

func NewService(cargos cargo.Repository) Service {
    return &service {
        cargos: cargos,
    }
}

NewService  simply returns a struct that implements the Service  interface. Now, of course, if I want to add stuff like logging and metrics, I really do not want to obscure the use case in the application service. Instead we create a new logging service that will decorate the original service.

type loggingService struct {
    logger log.Logger
    Service
}

Notice how the Service  field does not have a name. This is called type embedding and provides the struct with fields and methods from the embedded type. Think of it as a way of achieving inheritance through composition. By doing this, loggingService  will automatically implement the Service  interface and at the same time contain a reference to the original service implementation.
Now we can start overriding each method to provide logging.

func (s *loggingService) Track(id string) (c Cargo, err error) {
    defer func(begin time.Time) {
        s.logger.Log("method", "track", "tracking_id", id, "took", time.Since(begin), "err", err)
    }(time.Now())
    return s.Service.Track(id)
}

Of course, using this way of decorating services, you can add as much middleware functionality as you see fit, while elegantly separating each concern from the others. When setting things up you would simply do something like this:

var service Service
service = NewService(...)
service = NewLoggingService(logger, service)
service = NewInstrumentingService(..., service)
// ...

Obviously, this pattern is not specific to Go kit but it is a pattern that is used extensively as we will see next. It would be interesting to see how other frameworks deal with this.

Bounded contexts

Alright, people are using our service! Sadly though, we face another problem as the domain model continue to grow. The team is finding it increasingly hard to reason about some of the concepts in the application. In this case, the confusion is due to the fact that some of the concepts are only valid in a certain context. By recognizing this we can explicitly define these bounded contexts, in which we can be perfectly clear about how each individual concept is used, and how it is referred to.

In the sample application, this is demonstrated by the use of an external routing service. Both services deal with the concept of routes, but the team has realized that they are not actually referring to the same thing. The routing service provides a complex optimization algorithm to generate different paths between locations, whereas the main service cares about providing means to update itineraries in case a customer wants to change destination of the cargo after it has been shipped. You can easily imagine how this could lead to confusion if the teams are not susceptible to these implicit variations in their application.

In this case, the main service communicates with the routing service through a domain service. Domain services belong to the domain model, and help us define concepts that are not really things in same way that a Cargo or an Itinerary is, e.g. generating a set of itineraries. Since generating these itineraries is a non-trivial computation, the implementation of the routing domain service, in this case, will actually be a proxy for another bounded context.

// package routing

type Service interface {
    FetchRoutesForSpecification(rs cargo.RouteSpecification) []cargo.Itinerary
}

type proxyService struct {
    context.Context
    FetchRoutesEndpoint endpoint.Endpoint
    Service
}

func (s proxyService) FetchRoutesForSpecification(rs cargo.RouteSpecification) []cargo.Itinerary {
    response, err := s.FetchRoutesEndpoint(s.Context, fetchRoutesRequest{
        From: string(rs.Origin),
        To:   string(rs.Destination),
    })
}

In the same way we did for the application services, we embed the service into a proxyService  that we also provide with an endpoint to the bounded context. As you can imagine, we can just as easily decorate the endpoint in the same manner with middleware like a circuit breaker for when the other service starts to misbehave.

var e endpoint.Endpoint
e = makeFetchRoutesEndpoint(ctx, proxyURL)
e = circuitbreaker.Hystrix("fetch-routes")(e)
return proxyService{ctx, e, next}

As you can see, decorating our domain- and application services like this is a really nice way to separating business logic from infrastructure.

Closing words

Sample applications are hard to get right. It needs to be simple enough for people to understand the concepts that are being demonstrated, but sufficiently complex in order to be realistic. Although I am using Go kit for this project, I want to stress that the sample application is not meant to be tied to any specific libraries or frameworks. Instead, I ask the reader to focus on the motivation behind the design choices I have mentioned, the structure and package organization as well as the separation of business logic from infrastructure.

Hopefully, this blog series have given you some ideas on how to get started using Go for a new range of applications. I believe Go is a great language for when implementing core business services. It offers a simple but powerful syntax that gets out of your way, and lets you focus on the things that matter to your business.

I would like to thank all of those who contributed to both the project as well as this blog series. Pull requests are most certainly welcome, but perhaps even more so, if you have thoughts on how to improve the example, let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *