Help with clean architecture for web application

Hello,

I’ve read a lot of articles (usually to simple to answer to what I’m wondering), read some books, pratice a lot but I have still questions about how to architecture a web application or at least, for a complex application, I’m finding myself writing functions that do too much things and I’m not able to refactor in a good way.

For my web application, I’m always using:

  • sqlc to generate the SQL boilerplate code
  • chi router or alternative
  • templ to build HTML with Go

So here is my project layout (approximatively):

cmd/   # main app here
internal/
    database/               # code to initialise database, create connections...
       db/migrations/*sql   # sql code migrations (for dbmate or alternatives)
    html
        handlers/           # all my handlers
        views/              # all the templ code to generate html
    middlewares/
    repository/             # code generated by sqlc
    ...
pkg/
    config                  # config package to handle the configuration of the app
    user                    # to handle users for examples
    token                   # to handle the auth of users for example
    subscription            # to handle the subscriptions of user
    ...
queries/                    # contains all requests that will be used by sqlc to generate the boilerplate code

Packages user, token, subscription handles generally the basic operations (create, fetch, delete, update). I’m writing all of them in the same way, it’s a service layer.

For example, for my user package, I have this:

package user
...

type Service struct {
	queries *repository.Queries
}

func NewService(db repository.DBTX) *Service {
	queries := repository.New(db)
	return &Service{
		queries: queries,
	}
}

func (s *Service) CreateNewUser(...)

// GetUserFromEmail returns the user with the given email
func (s *Service) GetUserFromEmail(ctx context.Context, email string) (repository.ClientsUser, error) {
	return s.queries.GetUserWithItsEmail(ctx, email)
}

...

Now for my handlers package, as they need to use all the services, the constructor can be huge. In my actual project, it’s something like this:

package handlers

func NewController(cfg config.ConfigApp, userSvc *user.Service,
	tokenSvc *token.Service, fileSvc *file.Service,
	countrySvc *country.Service, redisClient *redis.Client,
	store storage.Storage, tSvc *turnstile.TurnstileSvc,
	mailSvc mailservice.SendMailer,
	stripeSvc *stripesvc.StripeSvc,
	subscriptionSvc *subscription.Service,
	productSvc *product.Service) *Controller {
	return &Controller{
		cfg:             cfg,
		userSvc:         userSvc,
		tokenSvc:        tokenSvc,
		fileSvc:         fileSvc,
		countrySvc:      countrySvc,
		storage:         store,
		turnstileSvc:    tSvc,
		mailSvc:         mailSvc,
		stripeSvc:       stripeSvc,
		subscriptionSvc: subscriptionSvc,
		productSvc:      productSvc,
	}
}

...

Does it seem ok to have a such controller ? (It’s very anoying to write unit tests with that, even if I can pass nil sometimes)

And now is my main problem. I have business logic in my handler. For example, for a webapp that allow users to upload files. The business logic could be different if the user has a subscription or not, and be different in function of the type of the subscription.

I find myself writing the upload handler like that:

  • check JWT token is valid with tokensvc ( this part should be in a middleware, ok with that)
  • identify user from the email in the token with usersvc
  • retrieve the size of the request to check later if the subscription of the user allow to upload it
  • get the active product for the user
  • calculating the uploaded files to check if the user can upload the actual file
  • I have some business logic based on subscriptions
  • and here I handle the upload of the file

This handler use:

  • token service
  • subscription service
  • product service
  • user service

I’m wondering if the user service should not use itself subscriptions service, product service for example ? My main business logic is around that.

If you had the patience to read until here, thank you.

based on SOLID principle,
sounds you should
Aggregate related services into a cohesive module or service, which the controller can depend on. For example, if userSvc, tokenSvc, fileSvc, and countrySvc are closely related, consider wrapping them in a composite service.

1 Like

It makes sense. Thank you. I don’t know how to name a such service, I will think about it. Thanks