Service Layer (Usecases)
Directory: article/
The Service Layer (also known as the Usecase or Application Business Logic layer) is responsible for implementing the specific use cases of the application. It orchestrates the flow of data between the Domain entities and the data sources by using repository interfaces.
This layer acts as a mediator: the Delivery Layer calls it, and it uses the Repository Layer to get things done. Its core responsibility is to contain the business logic that is specific to this application.
Key Components
1. The Service Struct
The main component is the Service struct, which holds dependencies to the repository interfaces it needs to operate.
File: article/service.go
// ... imports
type Service struct {
articleRepo ArticleRepository
authorRepo AuthorRepository
}
// NewService will create a new article service object
func NewService(a ArticleRepository, ar AuthorRepository) *Service {
return &Service{
articleRepo: a,
authorRepo: ar,
}
}
Notice that Service depends on ArticleRepository and AuthorRepository, which are interfaces, not concrete types. This is key to the Dependency Inversion Principle.
2. Repository Interfaces
Within the Service Layer, we define the contracts for what the data persistence layer must provide. These are the Go interfaces that the Repository Layer will implement.
File: article/service.go
// ArticleRepository represent the article's repository contract
//go:generate mockery --name ArticleRepository
type ArticleRepository interface {
Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error)
GetByID(ctx context.Context, id int64) (domain.Article, error)
GetByTitle(ctx context.Context, title string) (domain.Article, error)
Update(ctx context.Context, ar *domain.Article) error
Store(ctx context.Context, a *domain.Article) error
Delete(ctx context.Context, id int64) error
}
// AuthorRepository represent the author's repository contract
//go:generate mockery --name AuthorRepository
type AuthorRepository interface {
GetByID(ctx context.Context, id int64) (domain.Author, error)
}
By defining these interfaces here, the Service Layer dictates its own needs without knowing anything about MySQL, PostgreSQL, or any other database.
3. Business Logic Implementation
The methods of the Service struct contain the core business logic. For example, the Fetch method not only fetches a list of articles but also enriches them with author details.
File: article/service.go
func (a *Service) Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) {
res, nextCursor, err = a.articleRepo.Fetch(ctx, cursor, num)
if err != nil {
return nil, "", err
}
res, err = a.fillAuthorDetails(ctx, res)
if err != nil {
nextCursor = ""
}
return
}
In this method:
- It calls
a.articleRepo.Fetch()to get a paginated list of articles. - It then calls a private helper method
a.fillAuthorDetails(), which concurrently fetches details for all authors associated with the articles.
This orchestration is a perfect example of application-specific business logic that belongs in the Service Layer.
Another example is the logic in the Store method, which checks for conflicts before creating a new article:
func (a *Service) Store(ctx context.Context, m *domain.Article) (err error) {
existedArticle, _ := a.GetByTitle(ctx, m.Title) // ignore if any error
if existedArticle != (domain.Article{}) {
return domain.ErrConflict
}
err = a.articleRepo.Store(ctx, m)
return
}