a bookstore with a spring boot backend, a react frontend, a redis cache and a pluggable persistence layer — postgresql by default, with a drop-in mongodb implementation selected by a spring profile.
- about the project
- domain description and core entities
- actors
- use-case diagram
- er diagram
- user scenarios
- business processes (bpmn)
- c4
- sequence diagrams
- database schema
- function algorithm flowcharts
- project structure
- stack
- persistence
- running
- research
a bookstore database with personal book recommendations based on what each user does. the project has two parts:
- bookstore — the list of books, authors, publishers, orders, wishlists and book collections made by users;
- recommendation system — user reviews and ratings, and the saved results of the filtering algorithms.
the database is in third normal form. it uses a role-based access model. thanks to indexing and an outside cache (redis), normal queries run in 100 ms or less.
the project uses two methods.
collaborative filtering — it finds users who are alike, using the ratings matrix. to measure how alike users
here calculate_recommendations), it picks up to 10 most alike users (correlation
here
content-based filtering — it matches a book's features against what the user likes. how well book
here
the final recommendation mixes both methods. for a book found by both, the scores are added with weights:
here bookstore.recommendation.collaborative-weight,
the domain is an online bookstore with personal book ideas.
core entities:
- user (user) — a person with an account in the system;
- book (book) — a book you can buy;
- author (author) — the author of a book;
- category (category) — a genre or section;
- publisher (publisher) — the publisher of a book;
- review (review) — a rating and comment on a book;
- order (order) — a purchase of one or more books;
- order item (orderitem) — one book in an order, with its quantity and price;
- wishlist (wishlist) — books the user wants to buy later;
- collection (collection) — a book collection made by a user, with its books (collection_books);
- recommendation (recommendation) — the saved result of the recommendation algorithm for a user.
| actor | description |
|---|---|
| guest (guest) | a user without an account. can look at the catalog, book pages, read reviews and use search. cannot get personal book ideas or place orders. |
| customer (customer) | a user with an account. can order books, leave reviews, get personal book ideas and keep a wishlist. |
| moderator (moderator) | a staff member. has all customer rights, and can also edit book entries, delete reviews that break the rules, and see overall statistics. |
| admin (admin) | full access to everything: managing users, roles, the catalog, categories, publishers and authors, clearing the cache, and viewing the action log. |
Diagram is in the Russian version.
Diagram is in the Russian version.
actor: customer (authenticated)
precondition: the user has left some reviews and made some purchases before.
main flow:
- the user opens the "recommendations" section.
- the system looks at the user's ratings and past purchases and builds a personal list of books.
- the user sees a list of suggested books with short notes.
- the user picks a book and opens its page.
- reads the description and reviews from other customers.
- clicks "order".
- the system makes a new order with the status "processing".
postcondition: an order is made. on the next request, the recommendations are updated to include the new purchase.
actor: customer (authenticated)
precondition: the user has already got and read the ordered book.
main flow:
- the user opens the book page and goes to the "reviews" section.
- fills in the form: picks a rating (1–5 stars) and writes a comment.
- sends the review.
- the system checks if the user has reviewed this book before:
- no — it makes a new review and works out the book's average rating again;
- yes — it updates the old review and works out the rating again.
- the review shows up in the list on the book page.
postcondition: the review is saved, the book's average rating is updated, and the recommendations are worked out again on the next request.
actor: moderator
precondition: someone has reported a review with offensive content.
main flow:
- the moderator opens the review management section.
- finds the review that breaks the service rules.
- clicks "delete review" and confirms.
- the system deletes the review and works out the average rating of that book again.
- the moderator gets a message that the delete worked.
postcondition: the review is deleted, the book's rating is worked out again, and the recommendations are updated on the next request.
actor: admin
precondition: many new books have been added to the catalog, so the old recommendations are out of date.
main flow:
- the admin opens the system management section.
- clicks "clear the recommendation cache".
- the system drops the old saved recommendations.
- the admin gets a message that the action worked.
- on the next request, the system builds the recommendations again, using the current catalog.
postcondition: the old recommendations are gone, and users get fresh personal lists.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
dbms: postgresql. Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
Diagram is in the Russian version.
code/
backend/ spring boot (java 17): domain, dto, repository (postgresql + mongodb), service, techui-консоль
frontend/ react + typescript (vite): слои domain / application / infrastructure / presentation
sql/ схема, индексы, триггеры, функции, роли, helper-функции генерации данных
scripts/ generate_data.py (наполнение бд), benchmark_indexes.py, plot_index_graphs.py
benchmarks/ результаты замеров индексов (csv/json)
docker-compose.yml, run.sh
diagrams/ исходники и png всех диаграмм (er, c4, bpmn, sequence, схема, алгоритмы, графики исследования)
run.sh локальный запуск без контейнеров приложения
- backend: java 17, spring boot 3, spring security, spring data jpa / spring data mongodb;
- database: postgresql 15 by default, or mongodb — see persistence;
- cache: redis 7;
- frontend: react 18 + typescript (vite), served through nginx in docker;
- infrastructure: docker compose (postgres, redis, api, nginx).
the services depend only on repository interfaces in ru.bookstore.repository, and the database is an interchangeable adapter. there are two complete implementations, picked by spring profile:
- postgresql (
repository/postgres,@Profile("postgres")) — spring data jpa entities, entity mappers and jpa repositories; - mongodb (
repository/mongo,@Profile("mongo")) — document models, mappers and spring data mongodb repositories (with a sequence service for numeric ids).
switching the store is just a profile change — no service, controller, dto or domain code is touched:
- default (postgres):
SPRING_PROFILES_ACTIVE=web,postgres(the value docker compose uses); - mongodb:
SPRING_PROFILES_ACTIVE=web,mongo, withspring.data.mongodb.uripointing at a mongodb instance (defaultmongodb://localhost:27017/bookstore).
the mongo profile also disables the jpa/datasource auto-configuration (see application-mongo.properties), so the app runs against mongodb alone. docker compose provisions postgresql out of the box; to run on mongodb, start a mongodb instance (locally or as an extra compose service) and activate the mongo profile.
you need docker (and python3 for the seed step). one command builds the backend and frontend images and starts postgres, redis, api and nginx with the ui:
./code/run.sh start # = docker compose up -d --build
./code/run.sh seed # тестовые данные (generate_data.py)
./code/run.sh logs [api|frontend|postgres|redis]
./code/run.sh stopthis is the same as: docker compose -f code/docker-compose.yml up -d --build.
after it starts:
- ui — http://localhost:3000 (nginx, sends
/apito the backend) - rest api — http://localhost:8080
- postgresql — localhost:5433 (database
bookstore_db) - redis — localhost:6379
test login after seed: admin / admin123.
without the application containers (you need java 17, maven, node/npm; postgres and redis run in docker). the frontend is built with vite straight into spring boot's static resources. the backend runs locally and serves the api and ui at http://localhost:8080:
./run.sh start
./run.sh seed
./run.sh stopfor the tests, the script makes a dataset, switches between index setups, runs sets of measurements, and saves the results together with the explain analyze plans.
after setup, the data size was:
users— 1000 rows;reviews— 29480;orders— 30000;order_items— 30000.
these setups were compared: no indexes, simple, composite, simple plus composite, redundant.
simple indexes were built for the columns category_id, user_id, created_at, avg_rating, order_id, publisher_id.
composite ones — for the sets (category_id, book_id), (user_id, rating), (user_id, status, created_at), (book_id, rating).
the redundant set added indexes for price, created_at, total_amount.
the queries that were measured:
- top books by category (q1);
- a user's reviews sorted by rating (q2);
- a user's orders by date (q3);
- rating statistics grouped by category (q4);
- a user's order history with a
joinon items (q5); - write: inserting an order and an item (with a transaction rollback) (q6).
for each "query-setup" pair, 20 warm-up runs and 250 measured runs were done.
before each set, the indexes were rebuilt to match the setup and analyze was run.
summary of run times, ms:
| index configuration | average read time | average write time |
|---|---|---|
| no indexes | 0.9068 | 5.7547 |
| simple indexes | 0.8125 | 6.0368 |
| composite indexes | 0.8525 | 6.0350 |
| simple and composite indexes | 0.8144 | 6.0965 |
| redundant indexing | 0.8622 | 6.3980 |
chart of the average run time of queries q1-q6:
Diagram is in the Russian version.
chart of the average run time by number of indexes:
Diagram is in the Russian version.
for the order-history query with a join, the plan with no indexes uses sequential scans, while the setups with simple indexes use index reads. this matches the lower read time in the measurements. with the redundant setup, the average read time goes up compared to the simple and combined indexes: some extra indexes are not used in the queries but still make planning and upkeep cost more.
the lowest average read time was with simple indexes — 0.8125 ms; for the "simple and composite indexes" set — 0.8144 ms; and with no indexes — 0.9068 ms. for writes, the lowest average time was 5.7547 ms with no indexes; with the "simple and composite indexes" set it goes up to 6.0965 ms, and with redundant indexing — to 6.3980 ms.
indexes make reads a lot faster (the best result is with simple indexes), but writes get slower. the "simple and composite indexes" set gives a read time close to the best, with only a small drop in write speed.