Skip to content

3. Use Cases

Following the BookingFly use case (.torpedo/use_cases/booking_fly.yaml), Torpedo will generate the skeleton code where the custom logic should be placed.

The use case dir struct is not mandatory, but it is strongly suggested as:

booking-fly/
  |_ .torpedo
  |_ dependency
  |_ domain
     |_ ...
     |_ use_cases
        |_ onboarding
           |_ inputs
              |_ http                  // dir to add your controllers logic and DTOs.
           |_ outputs
              |_ mongodb               // dir to put all your repository logic related to your use case.
           |_ testing
              |_ mocks                 // dir to add your custom JSON mocks files or others.
           |   
           |_ torpedo_use_case_base.go // autogenerated struct with defined entities dependencies.
           |_ use_case.go              // UseCase struct where your use case code must be writen
           |_ use_case_test.go         // use case tests

     |_ ...   

After get the use case code struct ready to use we should define its method or methods.

Domain Service

If we need to export our Domain Service the use case should be bound into the Service struct. Remember that Domain Service is implemented following the Facade pattern.

For further documentation please read Domain Service section

Let's start writing the use case

Adding use case method

Based on our Booking Fly App example, the auto generated BookingFly use case code looks like:

booking-fly/domain/use_cases/booking_fly/use_case.go
// Package booking_fly Fly reservation use case
package booking_fly

import (
"github.com/darksubmarine/torpedo-lib-go/context"
"github.com/darksubmarine/torpedo-lib-go/log"

"github.com/darksubmarine/booking-fly/domain/entities/user"
"github.com/darksubmarine/booking-fly/domain/entities/trip"
)

// UseCase struct that implements the use case business logic.
type UseCase struct {
    *UseCaseBase

    /*  Put here your custom use case attributes */
}

// NewUseCase creates a new instance.
func NewUseCase(logger log.ILogger, userSrv user.IService, tripSrv trip.IService) *UseCase {

    return &UseCase{
        UseCaseBase: NewUseCaseBase(logger,
            userSrv, tripSrv,
        )}
}

/*
    Write here all the methods to cover your use case logic!

func (uc *UseCase) YourAwesomeUseCase() error { return nil }
*/
Remember that our case is

Name: BookingFly

Description
Given a frequent flyer user should be able to do a booking fly from our well known fly routes, selecting the departure airport and the arrival airport, also setting up the from-to fly dates. If the booking is successful, so the system should calculate the user awards and upgrade it following the rules below:

Rule 1:
IF  the user.Plan is GOLD
    AND the user accumulated miles (current user miles + trip.Miles) are greater than 1500
    AND the trip.Miles are greater than 2000
THEN
    user.Miles = current user miles + trip.Miles + 1000
ELSE
    user.Miles = current user miles + trip.Miles + 200

Rule 2:
IF  the user.Plan is SILVER
    AND the user accumulated miles (current user miles + trip.Miles) are greater than 8000
THEN
    user.Miles = current user miles + trip.Miles + 500
    user.Plan = GOLD

Rule 3:
IF  the user.Plan is BRONZE
    AND the user accumulated miles (current user miles + trip.Miles) are greater than 4000
THEN
    user.Miles = current user miles + trip.Miles + 300
    user.Plan = SILVER

lets adding a method named DoBooking to our use case:

// ErrUserNotFound user not found with given Id
var ErrUserNotFound = errors.New("user not found with given Id") //(1)!

// DoBooking method that performs a trip reservation updating user plan and miles.
func (uc *UseCase) DoBooking(tripModel *trip.TripEntity) (*trip.TripEntity, error) {

    //1. creates the trip data
    tripCreated, err := uc.tripSrv.Create(context.NoOpDataMap, tripModel)
    if err != nil {
        uc.logger.Error("something was wrong at booking creation", "error", err)
        return nil, err
    }

    //2. fetch the user linked to the trip
    userModel, err := uc.userSrv.Read(context.NoOpDataMap, tripCreated.UserId())
    if err != nil {
        uc.logger.Error("something was wrong getting the booking user", "error", err)
        return nil, ErrUserNotFound
    }

    //3. prepare trip award information
    tripMiles := tripCreated.Miles()
    accumulatedMiles := userModel.Miles() + tripMiles
    awardMiles := accumulatedMiles
    userPlan := userModel.Plan()

    switch userPlan {
    case "GOLD":
        if accumulatedMiles > 15000 && tripMiles > 2000 {
            awardMiles += 1000
        } else {
            awardMiles += 200
        }
    case "SILVER":
        if accumulatedMiles > 8000 {
            awardMiles += 500
            userPlan = "GOLD"
        }
    case "BRONZE":
        if accumulatedMiles > 4000 {
            awardMiles += 300
            userPlan = "SILVER"
        }
    }

    userModel.SetMiles(awardMiles)
    userModel.SetPlan(userPlan)

    //4. save user with updated award information based on the previous rules.
    if _, err := uc.userSrv.Update(context.NoOpDataMap, userModel); err != nil {
        uc.logger.Error("something was wrong updating the user", "error", err)
        return nil, err
    }

    return tripCreated, nil
}

  1. This error and others useful to use case error handling can be placed within a file named errors.go. For illustrative purpose has been added as part of the use_case.go file.

Import errors package

After adding the method DoBooking check if the IDE added the errors package to imports section. If not add it by your own.

1
2
3
4
5
6
7
import (
    "errors"
    "github.com/darksubmarine/torpedo-lib-go/context"
    "github.com/darksubmarine/torpedo-lib-go/log"
    "github.com/darksubmarine/booking-fly/domain/entities/user"
    "github.com/darksubmarine/booking-fly/domain/entities/trip"
)

Adding use case endpoint to REST API

It is time to expose our use case via the REST API. In order to do it we need to create two files:

  • dto.go which is the input data model that we will be reading from the endpoint body
  • controller.go where the input logic will be placed, http function in this case.

So, let's create those files:

domain/use_cases/booking_fly/inputs/http/dto.go
package http

// BookingFlyDTO use case DTO to book a fly
type BookingFlyDTO struct {
    Departure_ *string `json:"departure,omitempty"`
    Arrival_   *string `json:"arrival,omitempty"`
    Miles_     *int64  `json:"miles,omitempty"`
    From_      *int64  `json:"from,omitempty"`
    To_        *int64  `json:"to,omitempty"`
    UserId_    *string `json:"userId,omitempty"`
} //@name useCases.BookingFlyDTO
domain/use_cases/booking_fly/inputs/http/controller.go
package http

import (
    "errors"
    "fmt"
    "github.com/darksubmarine/booking-fly/domain/entities/trip"
    tripHTTP "github.com/darksubmarine/booking-fly/domain/entities/trip/inputs/http/gin"
    "github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly"
    "github.com/darksubmarine/torpedo-lib-go/api"
    "github.com/darksubmarine/torpedo-lib-go/entity"
    "github.com/darksubmarine/torpedo-lib-go/log"
    "github.com/gin-gonic/gin"
    "net/http"
)

// Controller struct that handles HTTP Requests for the use case
type Controller struct {
    logger       log.ILogger
    ucBookingFly *booking_fly.UseCase //(1)!
}

// NewController controller constructor function
func NewController(useCase *booking_fly.UseCase, logger log.ILogger) *Controller {
    return &Controller{ucBookingFly: useCase, logger: logger}
}

// BookingFlyEndpoint HTTP handler function that calls the use case DoBooking method.
// @Summary Books a fly
// @Schemes http https
// @Description Books a fly given a user and the trip information
// @Tags UseCases
// @Accept json
// @Produce json
// @Param trip body BookingFlyDTO true "The user fly trip reservation"
// @Success 200 {object} trip.FullDTO
// @Failure 400 {object} api.Error
// @Failure 500 {object} api.Error
// @Router /booking [post]
func (c *Controller) BookingFlyEndpoint(ctx *gin.Context) {

    var dto BookingFlyDTO
    if err := ctx.ShouldBindJSON(&dto); err != nil {
        ctx.JSON(http.StatusBadRequest, api.ErrorBindingJSON(err))
        return
    }

    tripModel := trip.New()
    if err := entity.From(&dto, tripModel); err != nil {
        ctx.JSON(http.StatusBadRequest, api.ErrorBuildingEntityFromDTO(err))
        return
    }

    tripCreated, err := c.ucBookingFly.DoBooking(tripModel) //(2)!
    if err != nil {
        if errors.Is(err, booking_fly.ErrUserNotFound) { //(3)!
            ctx.JSON(http.StatusBadRequest, api.NewError("4007", err))
        } else {
            ctx.JSON(http.StatusInternalServerError, api.ErrorEntityCreation(err))
        }
        return
    }

    var tripDTO = tripHTTP.NewFullDTO()
    if err := entity.To(tripCreated, tripDTO); err != nil {
        ctx.JSON(http.StatusBadRequest, api.NewError("4006", fmt.Errorf("error copying model to DTO: %w", err)))
        return
    }

    ctx.JSON(http.StatusOK, tripDTO)
}
  1. Reference to the use case business logic.
  2. Calls to use case logic.
  3. Error handling based on use case error.

Now with the previous files created we can register it as part of the REST API within its own dependency module located in dependency/use_case_booking_fly.go.

dependency/use_case_booking_fly.go
package dependency

import (
    "github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly"
    BookingFlyHTTP "github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly/inputs/http" //(1)!
    "github.com/gin-gonic/gin" //(2)!

    "github.com/darksubmarine/booking-fly/domain/entities/trip"
    "github.com/darksubmarine/booking-fly/domain/entities/user"

    "github.com/darksubmarine/torpedo-lib-go/app"
    "github.com/darksubmarine/torpedo-lib-go/log"
)

type UseCaseBookingFlyProvider struct {
    app.BaseProvider

    // useCaseBookingFly provides an booking_fly.UseCase instance.
    useCaseBookingFly *booking_fly.UseCase `torpedo.di:"provide"`

    // logger instance provided by LoggerProvider.
    logger log.ILogger `torpedo.di:"bind"`

    // userSrv instance of user service.
    userSrv user.IService `torpedo.di:"bind"`

    // tripSrv instance of trip service.
    tripSrv trip.IService `torpedo.di:"bind"`

    // Uncomment following lines if your use case contains http input.
    // api router group to add endpoints under /api prefix
    apiV1 *gin.RouterGroup `torpedo.di:"bind,name=APIv1"` //(3)!
}

func NewUseCaseBookingFlyProvider() *UseCaseBookingFlyProvider {
return &UseCaseBookingFlyProvider{}
}

// Provide provides the use case instance.
func (p *UseCaseBookingFlyProvider) Provide(c app.IContainer) error {
    p.useCaseBookingFly = booking_fly.NewUseCase(p.logger, p.userSrv, p.tripSrv)

    p.apiV1.POST("/booking",
        BookingFlyHTTP.NewController(p.useCaseBookingFly, p.logger).BookingFlyEndpoint) //(4)!

    return nil
}
  1. Import the use case http package.
  2. Import the Gin Gonic dependency.
  3. Bind with the REST API instance.
  4. Adds the use case endpoint.