Shopping Cart Manager is a simple Go program that allows users to manage cart items.
The project is organized as follows:
> tree
.
├── cmd
│ ├── migration
│ │ └── schemas
│ ├── version.go
│ └── web-api
├── deployments
│ ├── docker
│ └── k8s
├── internal
│ ├── api
│ │ └── http
│ ├── config
│ ├── entities
│ ├── repositories
│ │ ├── cache
│ │ └── storage
│ └── services
└── pkg
├── mysql
└── redisCI for this project is managed using GitHub Actions. The process builds the Docker image and uploads it to the ghcr.io registry.
The application can be deployed using docker-compose:
- Local Development: Use the
.localcompose file to resolve dependencies locally. - Production: The
.prodcompose file is designed for deploying the application to a server.
Additionally, a Kubernetes setup (k8s/) is provided for deploying the application with helmsman and helm charts. The Kubernetes cluster is set up using Kind.
The API layer currently only supports HTTP handlers. Template files are embedded using the embed package, replacing the previous design that used absolute paths, which caused deployment issues.
Although Gin was chosen as the HTTP engine, other high-throughput frameworks like Fiber (based on fastHTTP) could be used for more demanding systems. However, Gin is sufficient for this application’s needs.
A custom middleware was developed to handle cookie checking for users, improving code organization and maintainability.
Note: The term
sessionwas incorrectly used for cookies in some parts of the code. This term was updated, but in the database,session_idremains unchanged for backward compatibility.
Configuration is managed using the envconfig package to avoid hardcoded values. For more complex use cases, tools like koanf could be used, but envconfig is sufficient for this project.
The services directory is designed to handle the business logic in the application, following Clean Architecture principles. The HTTP layer (e.g., Gin) is isolated from business logic, only handling queries, user input validation, and other presentation-layer concerns.
MySQL is used as the primary database. Changes include:
- Switching from
gormto raw SQL queries for improved performance and readability. - Developing migration logic to handle schema changes.
Benefits of using raw SQL:
- Readability: Queries are clearer and more intuitive.
- Performance: Raw SQL allows for better optimization, especially with complex queries.
- Flexibility: You can optimize SQL queries based on specific attributes.
- DBA Friendly: DBAs can easily understand and modify raw SQL.
Redis is used for caching cart items, reducing the need to access the primary database on every request. Since Redis is an in-memory store, read and write operations are much faster compared to traditional databases like MySQL.
In this project, the following structure is used:
/pkg: Contains general-purpose packages likemysqlandredis./internal: Contains business logic and project-specific code.
This structure is recommended by the Go community, separating general utility code from domain-specific logic.
To migrate the database schema, run the following command:
go run cmd/migration/main.go --direction upThis will apply the necessary schema changes.
Migration steps include:
- Adding a relationship constraint between
cart_items.cart_idandcart_entities.cart_id. - Creating a new
itemstable for better organization and to avoid hardcoded values in the code. - Ensuring
session_idis unique across tables. - Replacing the
totalcolumn incart_entitieswith a more normalized design. - Using
TIMESTAMPinstead ofDATETIMEfor better tracking of changes over time.
Several issues in the original schema required migration:
- The
cart_idin thecart_itemstable had no foreign key constraint. - The
itemstable was added to streamline thecart_itemsstructure. - The
session_idcolumn should be unique across all related tables. - The
totalcolumn incart_entitiesmade the table denormalized; this information can be derived from other related tables. - Using
TIMESTAMPinstead ofDATETIMEoffers better tracking of changes over time.
I have written unit tests for different modules, for mocking redis I have used miniredis and for mysql database I have used the package sqlmock, also for mocking api calls to this repositories I have used the testify package to mock the behavior of this repositories.
For the service layer and the item's service I have added some sample benchmark tests to measure the performance, altough this method uses the mocks and mocks do not represent real performance.
There is a functional test and an integration test in the tests directory which are very basic test cases spinned up by docker-compose (the one in deployments/docker/compose.local.yml). in the functional part we test the cache module and in the integration test we test the behavior of the whole application (set cookie and send the request to the applicaion, for more advanced integration tests we can examine the expectations like the value of items in redis and mysql by an actual redis and mysql driver and assert if something goes wrong).
I have also added k6 for the load testing.
So In my project, I have the following tests:
- unit tests
- benchmark tests
- load test
- integration tests
- functional tests
