I am trying to find a proper type for my IDs in a Go program designed using Uncle Bob Martin's "Clean Architecture".
type UserID ...
type User struct {
ID UserID
Username string
...
}
type UserRepository interface {
FindByID(id UserID) (*User, error)
...
}
I am following Uncle Bob Martin's "Clean Architecture", where the code is organized as a set of layers (from outside-in: infrastructure, interfaces, usecases, and domain). One of the principles is the Dependency Rule: source code dependencies can only point inwards.
My User
type is part of the domain layer and so the ID
type cannot be dependent on the database chosen for the UserRepository
; if I am using MongoDB, the ID might be an ObjectId
(string
), while in PostgreSQL, I might use an integer. The User
type in the domain layer cannot know what the implementing type will be.
Through dependency injection a real type (e.g. MongoUserRepository
) will implement the UserRepository
interface and provide the FindByID
method. Since this MongoUserRepository
will be defined in the interfaces or infrastructure layer, it can depend on the definition of UserRepository
in the (more inward) domain layer.
I considered using
type UserID interface{}
but then the compiler will not be very helpful if code in one of the outer layer tries to assign in incorrect implementation type.
I want to have the interfaces layer or infrastructure layer, where the database is specified, determine and require the specific type for UserID
, but I cannot have the domain layer code import that information, because that will violate the dependency rule.
I also considered (and am currently using)
type UserID interface {
String() string
}
but that assumes knowledge that the database will use strings for its IDs (I am using MongoDB with its ObjectId
-- a type synonym for string
).
How can I handle this problem in an idiomatic fashion while allowing the compiler to provide maximum type safety and not violate the dependency rule?
Maybe you can use something like this:
Then you assume that you are always passing and getting string as an ID (it can be stringified version of the integer ID in case of PgSQL and other RDBMSs), and you implement UserID per database type:
I wonder if this accomplishes what you want to achieve, but maybe it's more elegant to hide string conversions inside the UserID?