ThreeDotsLabs / wild-workouts-go-ddd-example

Go DDD example application. Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
https://threedots.tech
MIT License
5.04k stars 464 forks source link

ID foreign keys directly from clients or not? #31

Closed frederikhors closed 3 years ago

frederikhors commented 3 years ago

Prologue

Let's say I'm creating a simple invoice.

type Invoice struct {
    id        int
    createdAt time.Time
    updatedAt *time.Time
    date         time.Time
    note         *string
    number       int
    payed        bool
    amount       int
    customer     *Customer
    paymentType  *PaymentType
    tax          *Tax
    rows         []InvoiceRow
    // ... many others
}

func NewInvoice(
    id int,
    date time.Time,
    note *string,
    number int,
    payed bool,
    customer *Customer,
    paymentType *PaymentType,
    tax *Tax,
    rows []Row,
) (*Invoice, error) {
    i := &Invoice{
        date:         date,
        note:         note,
        number:       number,
        payed:        payed,
        customer:     customer,
        paymentType:  paymentType,
        tax:          tax,
        rows:         rows,
    }

    i.SetAmount()

    return i, nil
}

// ... all methods like ID(), Date() and so on...

// and methods that use the values of those structs, such as:

func (i *Invoice) SetAmount() {
    for x := range i.rows {
        i.amount += i.rows[x].Qty() * i.rows[x].Price()
    }

    i.amount += i.amount * i.Tax().Amount()
}

I'm using specific types for Invoice creation (directly from API port):

type CreateInvoiceRequest struct {
    Date          time.Time
    Note          *string
    Number        int
    Payed         bool
    CustomerID    int
    PaymentTypeID int
    TaxID         int
    Rows          []InvoiceRowInput
}

type InvoiceRowInput struct {
    ID          int
    Description *string
    Qty         int
    Price       int
    ProductID   *int
}

Then in my `app/command/invoice_create.go:

func (h CreateInvoiceHandler) Handle(ctx context.Context, createInvoiceRequest *CreateInvoiceRequest) (*domain.Invoice, error) {
    var domainInvoice *domain.Invoice

    domainInvoice, err := domain.NewInvoice(
        createInvoiceRequest.Date,
        createInvoiceRequest.Note,
        createInvoiceRequest.Number,
        createInvoiceRequest.Payed,
        createInvoiceRequest.CustomerID,
        createInvoiceRequest.PaymentTypeID,
        createInvoiceRequest.TaxID,
        createInvoiceRequest.Rows,
    )
    if err != nil {
        return nil, err
    }

    return h.repo.Create(ctx, createInvoiceRe)
}

Question

As you can see I'm getting from clients only the ID of customer, paymentType, tax, and so on...

But I use those structs value for my NewInvoice func.

Now I can fetch those values from repo using methods like yours:

UpdateTraining(
  ctx context.Context,
  trainingUUID string,
  user User,
  updateFn func(ctx context.Context, tr *Training) (*Training, error),
) error

in updateFn I can get each of them and only after I can use NewInvoice().

Or maybe I can change API and get that data from client (e.g.: Tax instead of taxID).

And maybe this is also a business doubt because maybe those ID references must be present and correct (in DB) at creation and not taken by the client itself or maybe not.

What do you think of how I am progressing? Do you have any other ideas?

Where am I doing wrong?

m110 commented 3 years ago

Hey @frederikhors!

As I mentioned in the previous discussion, I don't like the idea of calling another service for all missing details. It creates coupling between the services, and you could leak too much domain knowledge, so watch out for it.

Or maybe I can change API and get that data from client (e.g.: Tax instead of taxID).

I think you could consider this, but there are some complexities as well. I'd try to get only what's needed for the invoice. And watch out for the language boundaries. You now pass a customer, which probably has many details your invoice doesn't need. The invoice should need only a "buyer" information that includes address and tax details.

Not sure what's in PaymentType and Tax, do you store these in a database?

frederikhors commented 3 years ago

Not sure what's in PaymentType and Tax, do you store these in a database?

Yes, my head is still DB-oriented unfortunately.

THANKS!