eminetto / clean-architecture-go

Clean Architecture sample
628 stars 94 forks source link

Multi-tenancy logic related to repository, usecase or handler layer? #8

Open frederikhors opened 4 years ago

frederikhors commented 4 years ago

@eminetto thanks for your amazing work! Go is just a hobby for me and I'm having fun. I'm learning a lot from your project.

I'm trying to understand if multi-tenancy column/table based is something to be "included", if it is "related to" the repository, service or handler level.

Example

Adding tenant.go model like this:

package entity

type Tenant struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

to other models like this:

package entity

import "time"

type Bookmark struct {
    ID          ID        `json:"id"`
    TenantID    int64     `json:"tenant_id"` // <--- here
    Tenant      *Tenant   // <--- here
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Link        string    `json:"link"`
    Tags        []string  `json:"tags"`
    Favorite    bool      `json:"favorite"`
    CreatedAt   time.Time `json:"created_at"`
}

Let's talk for example of the Store() method:

Question

Let's say my tenant_id is a field of a User struct in context on every request (authenticated by a third party middleware).

Where do you think I should do something like below? In handler, service or repository?

tenantID := GetTenantIDFromUserInContext()
article.TenantID = tenantID

Doubts about fetch queries

Today, before I discover the amazing "clean architecture", I'm using a where clause in my SQL queries (https://github.com/go-pg/pg/issues/1179), like this:

// Used as: "q.Apply(FilterByTenant(ctx))"
func FilterByTenant(ctx context.Context) func(q *orm.Query) (*orm.Query, error) {
    user := ctx.Value(auth.CTXKeyUser).(*models.User)
    return func(q *orm.Query) (*orm.Query, error) {
        q = q.Where("tenant_id = ?", user.TenantID)
        return q, nil
    }
}

I think maybe the concept of FilterByTenant in the service layer is an unnecessary repetition and should belong to lower levels like repositories?

But I also think that the main multi-tenancy logic does not change with the change of possible repository types (Postgres, Mysql, Mongo, microservices).

What do you think about it?

eminetto commented 4 years ago

As far as I understood, your service need the tenant_id to work, no matter how you generate it. Here, as you are receiving this data from an HTTP request, your HTTP handler should get this from the context and send to the service.  In the Store function, you can create a test to validate the input, and if the tenant_id is missing return an error. If you implement another ways to receive the tenant_id, for instance, as a parameter in a CLI application, or from an environment variable, you won't need to alter the service and repository layers. This is the way I probably would implement a solution to this case :)