Parcels Management Service consists of the backend and the dashboard that will allow the customers to create shipments and bikers to pick and deliver them.
In this document I will give you an overview about the project, grab a cup of coffee ☕, there're a lot of technicalities and fun.
• A sender should be able to create a parcel to be delivered by specifying pick-up and drop-off address (should be just a text field, no need for address validation)
• A sender should be able to see the status of his parcels.
• A biker should be able to see a list of the parcels.
• A biker should be able to pick up a parcel.
• Once a parcel is picked up by a biker, it cannot be picked up by other bikers.
• A biker should be able to input the timestamp of the pickup and the delivery for each order.
• The status of the order should be updated for the sender.
In the rest of the document, I will be discussing how I tried to achieve the requirements and the technical decisions around it.
All of the system components is configured in the docker-compose.yaml
file, that you don't need anything other than
- Clone the project and
cd
into it - Docker compose up
- Enjoy!
The backend will be running at http://localhost:4040/api/
The frontend will be running at http://localhost:3005/auth/
For the customers login info:
user: customer1
, customer2
, customer3
, customer4
or customer5
pass: password
For the bikers login info:
user: biker1
, biker2
, biker3
, biker4
, biker5
, biker6
, biker7
, biker8
, biker9
or biker10
pass: password
Note: if you're willing to run the backend locally, without docker, don't forget to change the module alias configuration in package.json to:
"_moduleAliases": {
"@": "."
},
Since day 1, principles and goals were set to deliver a very high quality and fulfill the requirements in an elegant, and modern way with giving the testability and modularity the highest priority.
For the backend, I've used typescript
with express
, and tried as much as possible to follow the SOLID
Principles.
There're multiple folders and files created to preserve the modularity and for making classes and functions more focused adhering to the single responsibility
below is the file structure of the services, where each service have its tests
and repositories
implementations along with the other REST
needed files ( router, controller, validation )
More technical details and information are explained below.
Firstly, for the server instantiation, and the database connection creation, I've used the Singleton
pattern to have a single instance of each, at any time
parcels-delivery-service/backend/server.ts
Lines 3 to 25 in 4094ff4
parcels-delivery-service/backend/providers/Database.ts
Lines 7 to 34 in 4094ff4
The D
in SOLID
was one of the most principles that I was focusing on not to break, I've really experienced before, the mess that occurs when we decide after months of work, that we need to change the database for example! what a mess that would be. I've fallen into it one day.
thus for critical service I've been always keeping the Program to an interface not implementation
rule, in mind.
I've used https://github.com/microsoft/tsyringe from microsoft as a lightweight dependency injection
container, which made me able to inject the necessary services or modules in an easier, and controllable way.
parcels-delivery-service/backend/di/production.ts
Lines 10 to 26 in 4094ff4
Thanks to the decorators, and our base abstractions we can simply program to interfaces and make tsyringe
handles the rest.
The diagram below explains how these layers are communicating with each other, with the help of our container
:
By adhering to this architecture, the Business logic is fully isolated, and can be developed and tested independently, this can be seen
in the Biker.service
, Customer.service
or Shipment.service
, at which you will find that they're only applying business logic, and just communicating with the data layer interface ( not an implementation )
for example, the customer service
Such implementation of the services
with the Repository pattern
is making the domain
use-cases totally independent from the infrastructure
decisions, Because from our services point of view,
it does not matter if our data is being stored in PostgreSQL
, MongoDB
or even locally
, as long as we have a class that implements the interface and provides the methods we need everything is supposed to work.
And this's actually what I've made to Mock
the database in the e2e
tests.
Preserving the decoupling between the components, puts some challenges, as our services layer
, shouldn't know anything about the infrastructure layer
it shouldn't care or be impacted, if we used Express.js
or Nest.js
, a REST
API, or maybe just a plain socket layer as main communication channel!
at our case, i'm using REST APIs
, with express.js
, thus the controllers are looking as following, for example the shipment controller:
The service is injected, and is managed by the controller, this also have put some challenges into the services
implementation, as I needed to avoid throwing any HTTP
Exceptions from them, because if we later decided not to use REST
APIs, those errors from the services will break the decoupling, and we will then need to change the logic in the services, which's not really preferred IMO. We want to make the services fully isolated and independent.
Here's how I managed to avoid throwing errors in the services:
This's not the best way though, if I had more time, I'd rather prefer to have domain exceptions
, that can be thrown from the services, and then with a simple switch case for example, errors can be checked in the controllers:
const [status, error] = await this.shipmentService.matchShipment(shipmentInfo);
if (!status) {
switch (error) {
case instanceof NotFoundShipment:
throw new BaseError(409, 'Shipment is not found');
case instanceof NotAllowedForPickingShipment:
throw new BaseError(609, 'Shipment can\'t be picked');
}
}
Where NotFoundShipment
and NotAllowedForPickingShipment
are domain exceptions, this way, the service is fully independent and don't need to know anything about the higher modules and layers, and in fact also if I've had more time I'd prefer to implement a more well-typed errors and success responses to returned from the services. Thanks to khalil stemmler, he have explained it very well here https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/.
I've put a huge efforts into testing to make the code fully covered, and that's what i've actually learned from contributing to open source, tests
is one of the most important aspect, tests make us able to fix, debug, and add features more faster and easier, with being sure we didn't break anything.
I've written almost 42 test case, and made it possible to run them against a real database or independently without the need of database connection ( thanks to DIP
).
You can run the following commands for testing:
parcels-delivery-service/backend/package.json
Lines 30 to 36 in 4094ff4
npm run test:e2e // for testing without database
npm run test:integration // communicating with a real database
npm run test:cov // will run both tests and gather the coverage reports.
Note: before running the commands on your local machine, you need to change the module alias configuration in the package.json to:
"_moduleAliases": {
"@": "."
},
For the frontend side, I've used REACT
, SCSS
and Typescript
, responsitivity and modularity were kept in mind too while implementing the frontend dashboard, I've tried as much as possible to divide THE files and the components in a well-structured manner, for better reusability of the shared components and for making it easier to develop and maintain the code.
I've also used some components from Material UI
Below is the file structure of the application:
I've used the useReducer
hook along with the Context
for the state management across the application, and the simple useState
hook, for in-component state. The contexts are defined in the contexts
folder, and the providers with the reducers are defined in the providers
folder
I've put all of the shared components under the components/shared
folder, and kept the pages as simple as possible and moved all the forms and complex views to the components
page as well.
for the containers, I'm using them for grouping the related paths and pages together, so I can simply apply the protection
components against them easily
The main app router, pointing to the container with applying the required protection rules using the protection components:
parcels-delivery-service/frontend/src/App.tsx
Lines 8 to 23 in 42b2bb5
The containers:
And lastly the protection components:
-
Guest pages: This helper component will not allow authenticated users from opening/navigating/routing to the routes that are allowed only for guests ( a logged in user for example shouldn't be allowed to navigate to the auth pages ).
parcels-delivery-service/frontend/src/protection/guestRoute.tsx
Lines 7 to 23 in 42b2bb5
-
Protected pages: This helper component will not allow the guest users from opening/navigating/routing to the pages that require authentication ( dashboard ).
-
Allowed for specific roles pages: This helper component will only allow the authorized users to opening/navigating/routing to their specific pages ( a biker shouldn't be allowed to navigate to the customers routes - new shipment route - for example. )
I've created a service layer to manage the communication with the APIs, by creating a base HTTP
abstract class, that's extended by each service:
The base HTTP Class:
parcels-delivery-service/frontend/src/services/base.ts
Lines 4 to 22 in 917827d
The bikerServices
class for example:
parcels-delivery-service/frontend/src/services/biker/index.ts
Lines 5 to 11 in 917827d
All of the work mentioned above have been done in the past 4 days, I'm pretty sure there're a lot of areas, aspects, and decisions that can be improved. but the time was the main constraint:
-
The most important one of them, is the types, I've used
any
at many places because having a proper and correct types would time, so at some places, I decided to useany
, if I had more time i'd ensure that everything is well typed. -
I've also planned to add a
Continious integration
, so thee2e
tests can run on each commit / pull request, which's possible due to the fact that we're testing against a mocked database. -
The error handling and the domain exceptions, as mentioned above.
-
Using
class-validator
and data-transfer-objects (DTOs) instead of JOI. -
Adding
Swagger
documentation for the APIs -
Moving the errors messages to a single file, instead of having them directly set in-place.
-
Using
socket
for the shipments status updates, so the biker and the customer can see the changes in real time -
Focusing in decreasing the large components files, by splitting them into more shared components.
-
I'm not that good at
UX
, I'm pretty sure the dashboard components is subject to a lot of improvements too. -
Applying DDD techniques to have the domain fully isolated, for the current implementation, I've only focused into having the use-cases isolated, but in fact, there're no domain objects involved, this's huge to be implemented in 4 days, I've planned to work on it, but then realized that this will not be finished in 4 days, thus decided to relax the complexity and focus on the use-cases. Btw, here's a sketch of the initial plan before relaxing the complexity ( just initial thoughts, not completed and might have some business-defects ).
That's it think. I've really enjoyed working on this, and spent too much time ensuring and caring about the testability and the code quality.
You might also love to take a look on some of the other open-source sample projects that I created recently:
- Learning Platform (
Javascript
withExpress
): https://github.com/iifawzi/learning-platform-backend - Chatting System (
Typescript
withNest.js
) : https://github.com/iifawzi/nestjs-microservices-kafka
Let's always hope we keep learning, love what we're doing, and more importantly, caring about our users.
If you reached this section, thank you for your time going through it, I hope you enjoyed it.
Thank you!