1

My application uses 3 types of User : Students, Teachers & Admins.

The User base class serves as a mechanism for authentication, and I've decided for this project to use apikeys (note : it's a demo project).

Hence, there's an Apikey entity (living in its own table) and the User base class has a OneToOne mapping to a single Apikey.

I'm using mapped superclasses, so my database will contain four tables :

  • instructor
  • admin
  • student
  • apikey

In my registration process, apikeys are created, and a verification must be done to ensure no two apikeys exist with the same token. The table is annotated with a "unique" constraint but I want to perform this verification as well in my application services layer.

For that, I'd need an Apikey repository to verify that the apikey I've created (through a random generator) is indeed unique. But Apikey is an entity, and it's common wisdom not to create repository for entities as that may violate the consistency boundary of the aggregate.

And I can't make Apikey an Aggregate itself, because it makes no sense for an Apikey to exist outside of an Instructor/Admin/Student. An apikey necessarily belongs to either of these users.

So my questions are :

  • Is it fine to create a repository for apikeys in this case ? Are there any negative consequences to that design ?
  • Or should I embed a findByApikey method in each repository (InstructorRepository, AdminRepository, StudentRepository), and have my ApikeyGeneratorService depends on these three repository, and add one method generateApikeyFor(Student|Admin|Teacher) for each specific use case ? That seems very redundant.
  • Or should I create a root UserRepository with a single method isApikeyAvailable ? That repository would have no other purpose because User is an abstract class and there's no "User" in existence per se.

Note that I absolutely want to perform this verification in my code for this particular project.

Also, note that this project is developed in TypeScript with MikroORM.

9
  • What's wrong with adding a method to each repository to check the API key? Apr 18 at 18:12
  • 1
    Why do you want to perform the same verification twice? Just react to the database error. It is so much more efficient (because conflict is unlikely to happen) and doesn't require additional design.
    – freakish
    Apr 18 at 18:14
  • What's wrong with having a UserRepository be the base class for other ones? You don't need a "User" entity to have a common parent class for repositories. Apr 18 at 18:15
  • 1
    I try to keep domain models focused on business logic and keep authentication out of it. Whether a user is authorized to invoke certain domain functionality is a concern for the API layer.
    – Rik D
    Apr 18 at 19:38
  • 1
    Frame challenge: Might it be possible that one user of your application is both an Instructor and an Administrator? Or, slightly more far-fetched but usually not impossible, that one user is both an Instructor and a Student? Apr 19 at 6:31

1 Answer 1

7

You're getting sidetracked by a side issue to your overall architecture here.

First of all, let me point this out:

my database will contain four tables : instructor / admin / student / apikey

the User base class has a OneToOne mapping to a single Apikey

Relational databases have a habit of taking on a shape similar to that of the domain they're persisting, because it allows your database to do some of the straightforward filtering and data manipulation for you. This saves you some bandwidth and not loading more data from the external DB server than you realistically need.

However, the User class does not live in your database, which means that your codebase is going to be carrying 100% of the load that converts your reusable user logic into instructor/admin/student-specific logic. There's no way around this, because your database is not aware that instructor/admin/student share an ancestry.

The User base class serves as a mechanism for authentication

The consequence is that you cannot write reusable logic to apply to all three of these tables. You will have to repeat yourself to some degree.

I'm not saying you have to use Entity Framework, but I would suggest reading up on how Entity Framework handles inheritance. Specifically, you are currently using a table-per-concrete-type (TPC) approach, but I think that table-per-type (TPT) makes more sense for your use case.

This means you would have a separate User table which contains all the shared ancestry between instructor/admin/student, which would also handle the API key foreign key.

it's common wisdom not to create repository for entities as that may violate the consistency boundary of the aggregate

Smells like dogma to me, plus I think it misses the point of domain-driven design. The domain designs its aggregates, but the domain does not design the implementation of the persistence layer.

There is nothing stopping you from creating a persistence-private ApiKeyRepository and have your publicly accessible repositories inject it, and pass the buck to that private one when needing to deal with an API key CRUD operation.
Effectively, every public repository will then be able to individually tell you if your API key is globally unique (i.e. across instructor/admin/student)

2
  • So if i'm understanding correctly, i'd have one table User with the common machinery and three tables Admin/Teacher/Student with each their own information, and a OneToOne relationship from Admin/Teach/Student to User.
    – ancyrweb
    Apr 19 at 1:21
  • 2
    @ancyrweb: Some minor details aside, yes. The four tables will strongly mirror where you find your properties in your classes (a property in User will be found in the user table, etc). This also can enable you to (if you want it) generate a user-unique ID for all your admins/teachers/students, so you don't run into cases where all three tables have overlapping ID values; which will significantly simplify any user-oriented logic that you wish to write that relies on a given ID without needing to guess which table it might be from.
    – Flater
    Apr 19 at 4:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.