Generic ID type for "Clean Architecture" Go program

1.9k Views Asked by At

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?

3

There are 3 best solutions below

4
On BEST ANSWER

Maybe you can use something like this:

type UserID interface {
  GetValue() string
  SetValue(string)
}

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:

type PgUserID struct {
    value int
}

func (id *PgUserID) GetValue() string {
    return strconv.Itoa(id.value)
}

func (id *PgUserID) SetValue(val string){
    id.value = strconv.Atoi(val)
}

type MongoUserID struct {
    value string
}

func (id *MongoUserID) GetValue() string {
    return value
}

func (id *MongoUserID) SetValue(val string){
    id.value = val
}

I wonder if this accomplishes what you want to achieve, but maybe it's more elegant to hide string conversions inside the UserID?

2
On

Do User really need to be coupled with the identity?

I don't know if this is brilliant or stupid but I have stopped adding identity attribute in the data classes and only use it as a way to access information, much like you would do with a map or dictionary.

This allows me to pick one type of identity (UUID) that is convenient when I test my use-cases (and write a mock implementation for the repository interface) and another type of identity (Integer) that is convenient when I communicate with the database.

Either way, my data class stays the same which I have found to be very convenient.

(At some point I did keep the identity as a generic in my data class but I think something - perhaps the implementation of equals and hashCode, can't remember - forced me to decide the type of identity)

I mostly code in Java and don't know anything about Go but in Java I would use:

public class User {
    private final String username;
    ...
}

public interface UserRepository<K> {
    public User findById(K identity)
}

and tests on my use-cases that depends on UserRepository would use a mock implementation of UserRepository with UUID as type for identity:

public class UserRepositoryMock implements UserRepository<UUID> {
    public User findById(UUID identity) {
        ...
    }
}

but my implementation of UserRepository against the actual database would use Integer as type for identity:

public class UserSQLRepository implements UserRepository<Integer> {
    public User findById(Integer identity) {
        ...
    }
}

Clarification:

I want full control of primary keys so I don't allow them outside the database-adapter.

The system still needs to be able to identify entities so I have to introduce something else. Often this is just a simple UUID.

This isn't free of charge. In the database-adapter I have to introduce conversion between primary keys and identities. So both are stored as columns in the tables. It also means I will have to do one extra read from database before I can use primary keys in queries.

(I've only worked with smaller systems so I don't know if this would be an acceptable trade-off in larger systems)

0
On

I had the same issues, and based on some reading, this is what I am planning to use.

  • Keep the ID generation outside of Domain.
  • Since repository handles all entries, and it makes sense for the repository to generate new IDs.
class UserId:
    # Hides what kind of unique-id is being used and lets you change it without affecting the domain logic.
    pass


class User:
    uid: UserId
    name: str


class UserRepository:
    def next_id() -> UserId:
        # Can use UUID generator
        # Or database based sequence number
        pass


class UserUsecase:
    def __init__(self, repository):
        self.repository = repository

    def create(self, name):
        user_id = self.repository.next_id()
        user = User(user_id, name)
        self.repository.save(user)
        return user

Reference: https://matthiasnoback.nl/2018/05/when-and-where-to-determine-the-id-of-an-entity/