In our codebase at work, we had classes that mediated access to the DB (a stateless 'repository' pattern). This would expose methods for fetching from or updating the DB, usually for a single table, but sometimes for multiple related tables.
Sometimes, rather than a single class for accessing everything related to one table, we would use more specific classes, e.g GetUserEmail, UpdateUserEmail.
Instead of a class that exposes a broad set of methods for accessing a table, these classes expose access to a single column or a set of related columns.
And moreover, they separate read-access and write-access into different classes.
A service class close to the boundary of the system (e.g one used by an API endpoint controller) can take these in its constructor, and as long as the class doesn't try to access the DB internally through other classes, objects, or functions, then all access to the DB will be mediated through the repository classes it receives (think object capabilities and the principle of least privilege).
However, these repository classes, whether broad or limited, only slice access to the DB in what I'll call a 'column-based' way: they expose access to a limited set of tables, or a limited set of columns within those tables, and they may only allow read or write access, however they allow it for all rows. They limit the set of operations that can be performed but not the data on which the operations can act.
Another axis to consider is 'row-based' access.
We could have a class limit access to a set of rows.
For example, the GetUserEmail class could take a set of user ids in its constructor and only allow access to rows corresponding to those user ids.
However, it requires moving the repository objects that a service class receives out of the constructor and into each method. In some ways, this is actually nicer, it's closer to the ideal of the principle of least privilege: rather than the entire service class having access to a repository object, it's only the methods requiring that access that receive the object.
But, at least for our codebase, it seemed that this would be a larger refactoring. Now, when an API controller calls a service class method, it first has to construct a repository object, perhaps itself requiring other repository objects.
Instead, I decided to try decoupling the column-based and row-based access into two separate concepts:
- Repository objects for column-based access
- 'Handle' objects for row-based access
I introduced a 'UserHandle' class.
This is a simple wrapper around a UserId class, but the meaning is quite different.
While a UserId class just represents identity, a user id, and nothing else, the UserHandle class represents the authority to access the data corresponding to the given user id.
Now, a method of a repository class that would previously have taken a UserId can now require a UserHandle instead.
This approach requires diligence: we have to be careful to make sure all repository classes that access user data require a UserHandle, but it's still a nice improvement.
To prevent being able to create a UserHandle out of thin air, I made its constructor private and created a factory method UserHandle.mint that requires an AllUserCap object.
The AllUserCap class is similar -- it has a private constructor with a factory method AllUserCap.acquire.
The AllUserCap factory method takes an AmbientAuthority object.
The AmbientAuthority object in turn has a factory method AmbientAuthority.acquire.
Ideally, the AmbientAuthority factory method would not be callable from outside the entrypoint of the program, however that's not enforceable in the language we use (Kotlin), so instead we just rely on developer discipline and code reviews.