- Spin up a docker container with a mysql database
docker run \
--detach \
--rm \
--name blp-mysql \
-e MYSQL_ROOT_PASSWORD=my-secret-pw \
-e MYSQL_DATABASE=blp-coding-challenge \
-e MYSQL_USER=blp \
-e MYSQL_PASSWORD=password \
--volume=$(pwd)/db/initdb:/docker-entrypoint-initdb.d \
--publish 3306:3306 \
mysql:8.0- Look around
$> docker exec -it blp-mysql bash
bash-4.4# mysql -ublp -ppassword
mysql> use blp-coding-challengeThe goal of the challenge is to write server logic (not an actual http server) that can manage users, user groups and control access to them.
The challenge should be completed without using libraries other than the standard library.
The challenge is divided into stages where each stage builds on the previous stage and can be evaluated separately.
In the file pkg/server/interface.go you will find interfaces that the server needs to implement in order to pass each stage (don't change the interfaces).
In order to define an SQL database schema add statements to db/initdb/db.sql.
From the root of the repository, run the following commands:
docker compose up -d
go test ./...This project marks my first experience with Golang, and it has been super fun, the challenge is very interesting. While solving the challenge, I experimented with different approaches applied software design practices to build a clean, testable solution.
I recommend you start reviewing the code from server_test.go
I applied an outside-in TDD approach (London School of TDD), starting from integration tests against the server and then moving inward to the application layer.
- Integration tests: The server is the system under test, using real persistence repositories and a real UnitOfWork implementation.
- Unit tests: Cover both domain and application layers.
- I avoided verifying internal interactions (e.g. checking if a specific service was called with certain arguments), as I wanted tests to validate behavior, not implementation.
- Initially, I wrote narrow integration tests for the MySQL repositories, but given time constraints, I chose to rely on server-level integration tests, with the trade-off of losing test granularity in favor of delivery speed. If you see this as a problem I can show you how I'd have done it.
- Domain model with aggregates: Implemented User and UserGroup aggregates, applying dependency inversion to keep domain logic isolated from infrastructure concerns.
- Group To Group Relations: restricted to just have 1 parent for avoiding cyclic refernces and build a group tree that can be traversed in BFS
- Repository and Unit of Work patterns: Used to coordinate persistence and maintain consistency across operations.
- The current Unit of Work implementation is intentionally minimal. Areas for future improvement include:
- Allowing aggregate tracking to be disabled
- Separating queries from commands (Command-query separation CQS)
- Queries: do not begin the unit of work and tracking is disabled.
- Commands: open the unit of work and tracking is enabled.
- With a chain of responsibility pattern can open and close uow automatically only for commands
- Refining the Unit of Work internals
- User permission projections: It's crucial for the system scalability the use projections for an eventually-consistent approach, where permissions are preprocessed by userID, everytime there are permissions added/removed. That'd result in an important performance improvement with the price of having a delay between the addition/future removal of permissions and the their application (SLAs to agree). That'd apply to all kind or permissions (user-user, group-user, group-group, group-user). Storage: UserId -> (allowed to access) -> List of user IDs.
- Other problems may arise from this implementation that would need to take into consideration (out of order events, consistency, etc, event store with stable-hashing sharding?). Involves considering what the stale permissions risk analysis.
- Configurable Unit of Work tracking: Make aggregate tracking optional, will result in a big optimization
- Performance optimizations: Some operations (e.g., transitive queries on UserGroup) currently require multiple round trips. Dedicated repository methods could improve performance.
- Optimistic concurrency control: Ensure aggregates are updated only if their version matches the version read, protecting invariants and improving consistency