I have a question about using interfaces when separating business and other layers (such as the database layer in my case). I’ve heard that it is advisable to define interfaces in the same place where they are used.
In my application, I have a RegistryService
that encapsulates the logic for registering and deregistering workers in my application cluster. I also have a Memberlist
structure that stores my models in a linked list.
In my implementation of RegistryService
, I declare a workerProvider
interface with methods like SaveWorker
, DeleteWorkerByID
, etc. These methods return errors. Currently, these errors are defined in the memberlist
package. However, I want to abstract these errors so that they are not tightly coupled to the memberlist
implementation.
Should I move the workerProvider
interface to a separate package and define new error types in it?
package registry
import ( ... )
var (
ErrWorkerAlreadyRegistered = errors.New("worker is already registered")
)
type workersProvider interface {
SaveWorker(ctx context.Context, endpoint string, zone string) (domain.Worker, error)
DeleteWorkerByID(ctx context.Context, id uuid.UUID) (domain.Worker, error)
ListWorkers(ctx context.Context) ([]domain.Worker, error)
FindWorkerByID(ctx context.Context, id uuid.UUID) (domain.Worker, error)
}
type RegistryService struct {
log *slog.Logger
wp workersProvider
}
func NewRegistryService(log *slog.Logger, wp workersProvider) *RegistryService {
return &RegistryService{
log: log,
wp: wp,
}
}
func (reg *RegistryService) RegisterWorker(ctx context.Context, endpoint string, zone string) (domain.Worker, error) {
worker, err := reg.wp.SaveWorker(ctx, endpoint, zone)
if err != nil {
emptyWorker := domain.Worker{}
switch {
case errors.Is(err, memberlist.ErrWorkerAlreadyExists):
return emptyWorker, ErrWorkerAlreadyRegistered
default:
return emptyWorker, lib.WrapError(op, err)
}
}
return worker, nil
}
package memberlist
import (...)
var (
...
ErrWorkerAlreadyExists = errors.New("worker already exists")
...
)
type Memberlist struct { ... }
func New() *Memberlist { ... }
func (mlist *Memberlist) SaveWorker(_ context.Context, endpoint string, zone string) (domain.Worker, error) {
...
return domain.Worker{}, ErrWorkerAlreadyExists
...
}
func (reg *Memberlist) DeleteWorkerByID(_ context.Context, id uuid.UUID) (domain.Worker, error) { ... }
func (reg *Memberlist) ListWorkers(_ context.Context) ([]domain.Worker, error) { ... }
func (reg *Memberlist) FindWorkerByID(_ context.Context, id uuid.UUID) (domain.Worker, error) { ... }
I was always encouraged to try out my ideas and it made me learn a lot of things, so my suggestion is that you first write tests before making changes. Then you can create your package and see if it fits the design and future goals that you have in mind.
But also ask yourself what problems are you facing if the errors are coupled with memberlist? Does it prevent you from implementing some new feature ? Is the current design too complicated to work with ? These questions will help you take a better decision for yourself