go-clean-arch icon indicating copy to clipboard operation
go-clean-arch copied to clipboard

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

Open frederikhors opened this issue 5 years ago • 3 comments

@bxcodec 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, usecase or handler level.

Example

Adding tenant.go model like this:

package models

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

to other models like this:

package models

type Author struct {
	ID        int64 `json:"id"`
	TenantID  int64 `json:"tenant_id"` // <--- here
	Tenant    *Tenant // <--- here
	Name      string `json:"name"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

and

package models

import "time"

type Article struct {
	ID        int64 `json:"id"`
	TenantID  int64 `json:"tenant_id"` // <--- here
	Tenant    *Tenant // <--- here
	Title     string    `json:"title" validate:"required"`
	Content   string    `json:"content" validate:"required"`
	Author    Author    `json:"author"`
	UpdatedAt time.Time `json:"updated_at"`
	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, usecase 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 usecase 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?

frederikhors avatar Feb 14 '20 12:02 frederikhors

Hi @frederikhors sorry for late reply, so busy lately, hope you doing good.

I'll try to answer your question based on what I've done.

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

tenantID := GetTenantIDFromUserInContext()
article.TenantID = tenantID

I will put it in the usecase. Is context here means like the context.Context package? Or something else?

Have you thought about user-repository? And that repo will be injected to the article or whatever who need the details? like a tenant or stuff?

bxcodec avatar May 15 '20 08:05 bxcodec

Is context here means like the context.Context package?

Yep.

Have you thought about user-repository? And that repo will be injected to the article or whatever who need the details? like a tenant or stuff?

I don't understand the question.

frederikhors avatar Jul 04 '20 12:07 frederikhors

Oh my god, I missed this. So sorry @frederikhors tough time back then.

Let's schedule some time to discuss this properly. I want to learn how's your approach to the solution, it may be useful to add other use cases in this repo?

bxcodec avatar Aug 04 '22 01:08 bxcodec