A REST API for managing an online shopping basket with CQRS architecture - using DDD principles.
Please refer to the Refactorings Documentation for details on how I would extend and improve this API beyond the scope of this technical exercise.
- Add an item to the basket
- Add multiple items to the basket
- Remove an item from the basket
- Add multiple of the same item to the basket
- Get the total cost for the basket (including 20% VAT)
- Get the total cost without VAT
- Add a discounted item to the basket
- Add a discount code to the basket (excluding discounted items)
- Add shipping cost to the UK
- Add shipping cost to other countries
This API follows Clean Architecture principles with CQRS (Command Query Responsibility Segregation) pattern and Domain-Driven Design (DDD) concepts.
- CQRS: Separates read (Query) and write (Command) operations for better scalability and maintainability
- DDD: Domain entities encapsulate business logic, ensuring a rich domain model
- Repository Pattern: Abstracts data access logic from business logic
- Dependency Injection: Promotes loose coupling and testability
- Clean Architecture: Dependencies flow inward toward the domain layer
shopping-basket-api/
├── ShoppingBasket.Api/ # Presentation Layer
│ ├── Controllers/ # API endpoints
│ ├── Models/ # Request/Response models
│ └── Program.cs # Application entry point
│
├── ShoppingBasket.Application/ # Application Layer
│ ├── Command/
│ │ ├── Commands/ # Write operations
│ │ └── Handlers/ # Command handlers
│ ├── Query/
│ │ ├── Queries/ # Read operations
│ │ └── Handlers/ # Query handlers
│ ├── DTOs/ # Data Transfer Objects
│ ├── Mappers/ # Entity to DTO mapping
│ └── Interfaces/ # Application contracts
│
├── ShoppingBasket.Domain/ # Domain Layer
│ ├── Entities/ # Domain models
│ │ ├── Basket.cs # Aggregate root
│ │ └── BasketItem.cs # Entity
│ └── Interfaces/ # Domain contracts
│
├── ShoppingBasket.Infrastructure/ # Infrastructure Layer
│ ├── Data/
│ │ ├── ApplicationDbContext.cs # EF Core context
│ │ └── Migrations/ # Database migrations
│ └── Repositories/ # Data access implementations
│ ├── BasketRepository.cs
│ └── Interfaces/
│
├── ShoppingBasket.UnitTests/ # Unit Tests
│ ├── Domain/ # Domain logic tests
│ ├── Application/ # Handler tests
│ └── Api/ # Controller tests
│
├── docs/ # Documentation
│ └── refactorings.md # Future improvements
│
├── docker-compose.yml # Docker orchestration
├── Dockerfile # Container definition
└── README.md # This file
- .NET 8: Latest LTS version of .NET
- Entity Framework Core: ORM for data access
- SQL Server: Relational database
- xUnit: Unit testing framework
- Docker: Containerization
- Swagger/OpenAPI: API documentation
- CQRS: Command/Query separation (custom implementation)
CQRS allows us to:
- Optimize read and write operations independently
- Scale read and write workloads separately
- Maintain clear separation of concerns
- Simplify complex business logic
Domain-Driven Design helps us:
- Keep business logic in the domain layer
- Create a rich, expressive domain model
- Maintain consistency through aggregate roots
- Ensure entities are always in a valid state
- Docker Desktop (optional)
- .NET 8 SDK
- SQL Server or LocalDB
- Git
- Clone the repository
- Navigate to the project root directory
- Run the following command:
For Docker Desktop (latest) or Docker Compose V2:
docker compose up --buildThe API will be available at: http://localhost:5000
The database will automatically initialize with migrations on startup.
- Install SQL Server or SQL Server Express
- Update connection string in
appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=ShoppingBasketDb;Trusted_Connection=True;TrustServerCertificate=True;"
}- Run migrations:
dotnet ef database update --project ShoppingBasket.Infrastructure --startup-project ShoppingBasket.Api- Start the API:
dotnet run --project ShoppingBasket.ApiThe API will be available at: https://localhost:7000 or http://localhost:5000
Once the application is running, you can access the interactive API documentation at:
http://localhost:5000/swagger(Docker)https://localhost:7000/swagger(Local development)
Create a new basket
POST /api/basketsGet basket by ID
GET /api/baskets/{basketId}Get basket total
GET /api/baskets/{basketId}/totalAdd items to basket
POST /api/baskets/{basketId}/items
Content-Type: application/json
[
{
"name": "Product Name",
"price": 29.99,
"quantity": 2,
"isDiscounted": false
}
]Remove item from basket
DELETE /api/baskets/{basketId}/items/{basketItemId}Apply discount code to basket
POST /api/baskets/{basketId}/discountcode
Content-Type: application/json
{
"discountCode": "SAVE10"
}- Create a new basket:
curl -X POST http://localhost:5000/api/baskets- Add items to the basket:
curl -X POST http://localhost:5000/api/baskets/{basketId}/items \
-H "Content-Type: application/json" \
-d '[
{
"name": "Laptop",
"price": 999.99,
"quantity": 1,
"isDiscounted": false
},
{
"name": "Mouse",
"price": 29.99,
"quantity": 2,
"isDiscounted": true
}
]'- Apply a discount code:
- The only accepted discount codes are 'SAVE10', 'SAVE20' and 'SAVE30'.
curl -X POST http://localhost:5000/api/baskets/{basketId}/discountcode \
-H "Content-Type: application/json" \
-d '{"discountCode": "SAVE10"}'- Retrieve the basket:
curl -X GET http://localhost:5000/api/baskets/{basketId}- Get basket total:
curl -X GET http://localhost:5000/api/baskets/{basketId}/total- Remove an item:
curl -X DELETE http://localhost:5000/api/baskets/{basketId}/items/{basketItemId}Create Basket Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-10-20T10:30:00Z",
"updatedAt": "2024-10-20T10:30:00Z",
"discountCode": null,
"discountPercentage": 0.0,
"items": [],
"total": {
"totalExcludingVat": 0.0,
"totalIncludingVat": 0.0,
"vatAmount": 0.0,
"discountAmount": 0.0
}
}Get Basket Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-10-20T10:30:00Z",
"updatedAt": "2024-10-20T10:35:00Z",
"discountCode": null,
"discountPercentage": 0.0,
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174002",
"basketId": "123e4567-e89b-12d3-a456-426614174000",
"productName": "Laptop",
"quantity": 1,
"unitPrice": 999.99,
"addedAt": "2024-10-20T10:32:00Z"
},
{
"id": "123e4567-e89b-12d3-a456-426614174003",
"basketId": "123e4567-e89b-12d3-a456-426614174000",
"productName": "Mouse",
"quantity": 2,
"unitPrice": 29.99,
"addedAt": "2024-10-20T10:33:00Z"
}
],
"total": {
"totalExcludingVat": 1059.97,
"totalIncludingVat": 1271.96,
"vatAmount": 211.99,
"discountAmount": 0.0
}
}Add Items Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-10-20T10:30:00Z",
"updatedAt": "2024-10-20T10:35:00Z",
"discountCode": null,
"discountPercentage": 0.0,
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174002",
"basketId": "123e4567-e89b-12d3-a456-426614174000",
"productName": "Laptop",
"quantity": 1,
"unitPrice": 999.99,
"addedAt": "2024-10-20T10:32:00Z"
}
],
"total": {
"totalExcludingVat": 999.99,
"totalIncludingVat": 1199.99,
"vatAmount": 200.0,
"discountAmount": 0.0
}
}Apply Discount Code Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-10-20T10:30:00Z",
"updatedAt": "2024-10-20T10:40:00Z",
"discountCode": "SAVE10",
"discountPercentage": 10.0,
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174002",
"basketId": "123e4567-e89b-12d3-a456-426614174000",
"productName": "Laptop",
"quantity": 1,
"unitPrice": 999.99,
"addedAt": "2024-10-20T10:32:00Z"
}
],
"total": {
"totalExcludingVat": 899.99,
"totalIncludingVat": 1079.99,
"vatAmount": 180.0,
"discountAmount": 100.0
}
}Get Total Response:
{
"totalExcludingVat": 899.99,
"totalIncludingVat": 1079.99,
"vatAmount": 180.0,
"discountAmount": 100.0
}Error Response Example:
{
"error": "Basket ID must not be empty"
}200 OK- Successful GET requests201 Created- Successful basket creation204 No Content- Successful item removal400 Bad Request- Invalid request data or validation errors404 Not Found- Basket not found500 Internal Server Error- Server error
Given the 4-hour development timeframe:
- No authentication/authorization implemented
- Limited error handling and validation
- No rate limiting
- Basic logging implementation
- See refactorings.md for production-ready improvements
This project is part of a technical assessment and is not licensed for external use.