A full-stack e-commerce platform for authentic Andhra homemade pickles β built with Spring Boot, Thymeleaf web frontend, and a complete JWT-secured REST API backend. Containerized with Docker and deployed on Render.
π https://ammapickles-ecommerce.onrender.com
Note: Hosted on Render free tier β app may take 30β40 seconds to wake up on first visit.





Amma Pickles is a fully functional online store for ordering traditional Andhra pickles β Veg and Non-Veg varieties available in three sizes (Β½ kg, 1 kg, 2 kg). The project features a dual architecture: a Thymeleaf-rendered web UI for customers, and a JWT-secured REST API for external/mobile access.
Whatβs working:
This project uses a dual-layer architecture β both layers share the same service and repository code:
| Layer | Technology | Auth Method |
|---|---|---|
| Web Frontend | Thymeleaf + HTML/CSS | Session-based (Spring Security form login) |
| REST API | JSON responses | JWT Bearer Token (stateless) |
| Category | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.5.6 |
| ORM | Spring Data JPA / Hibernate |
| Database | MySQL 8.0 |
| Security | Spring Security (JWT + Session) |
| Frontend | Thymeleaf, HTML, CSS |
| Brevo SMTP | |
| Caching | Spring Cache |
| Build Tool | Maven |
| Containerization | Docker (multi-stage build) |
| Validation | Jakarta Bean Validation |
| Monitoring | Spring Boot Actuator |
| Utilities | Lombok, SLF4J |
| Optimization | Description |
|---|---|
| Spring Cache | Products cached in memory β reduces DB hits on every request |
| Cache Eviction | Cache auto-clears on admin add/update/delete β always fresh data |
| Lazy Initialization | Beans load on demand β reduces startup time by ~40% |
| Database Indexing | Indexes on name, category_id, composite β faster queries |
| Connection Pooling | HikariCP limited to 5 connections β protects free tier DB |
| open-in-view disabled | Prevents unnecessary DB connections during view rendering |
| Component | Platform |
|---|---|
| Application | Render (Docker container) |
| Database | Aiven MySQL |
| Containerization | Docker (multi-stage build) |
| Email Service | Brevo SMTP |
Docker multi-stage build β Stage 1 builds the jar using Maven, Stage 2 runs only the jar using lightweight JRE. Final image is small and production-ready.
src/main/
βββ java/com/ammapickles/backend/
β βββ AmmaPicklesApplication.java
β βββ config/
β β βββ AppConfig.java
β βββ controller/
β β βββ AuthController.java β REST: /api/auth/**
β β βββ AuthViewController.java β Web: /login, /register
β β βββ ProductController.java β REST: /api/products/**
β β βββ ProductViewController.java β Web: /products/{id}
β β βββ HomeViewController.java β Web: /home
β β βββ CartController.java β REST: /api/cart/**
β β βββ CartViewController.java β Web: /cart
β β βββ OrderController.java β REST: /api/orders/**
β β βββ OrderViewController.java β Web: /orders
β β βββ CategoryController.java β REST: /api/categories/**
β β βββ AddressController.java β REST: /api/addresses/**
β β βββ AddressViewController.java β Web: /addresses/**
β β βββ UserController.java β REST: /api/users/**
β βββ dto/
β βββ entity/
β βββ exception/
β βββ repository/
β βββ security/
β βββ service/
β
βββ resources/
βββ application.properties
βββ application.properties.example
βββ static/
β βββ css/style.css
β βββ images/default-product.png
βββ templates/
βββ home.html
βββ login.html
βββ register.html
βββ cart.html
βββ orders.html
βββ place-order.html
βββ product-detail.html
βββ add-address.html
βββ fragments/
βββ navbar.html
βββ footer.html
git clone https://github.com/reachraviraju/AmmaPickles-Ecommerce.git
cd AmmaPickles-Ecommerce
CREATE DATABASE amma_pickles;
USE amma_pickles;
INSERT INTO roles (name) VALUES ('ROLE_CUSTOMER');
INSERT INTO roles (name) VALUES ('ROLE_ADMIN');
cp src/main/resources/application.properties.example src/main/resources/application.properties
spring.application.name=AmmaPickles
server.port=8080
# MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/amma_pickles
spring.datasource.username=your_db_username
spring.datasource.password=your_db_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
# JWT (24 hours expiry)
jwt.secret=your_base64_encoded_secret_key
jwt.expiration=86400000
# Mail (Brevo SMTP)
spring.mail.host=smtp-relay.brevo.com
spring.mail.port=587
spring.mail.username=your_brevo_email
spring.mail.password=your_brevo_smtp_key
app.mail.from=your_sender_email
# Cache
spring.cache.type=simple
# Lazy initialization
spring.main.lazy-initialization=true
# Actuator
management.endpoints.web.exposure.include=health,info
mvn spring-boot:run
Visit: http://localhost:8080/home
docker build -t ammapickles .
docker run -p 8080:8080 ammapickles
| Route | Access |
|---|---|
/, /home, /products/** |
Public |
/login, /register |
Public |
/css/**, /images/**, /favicon.ico |
Public |
/cart/**, /orders/**, /addresses/** |
Authenticated (session) |
POST /login with fields username (email or phone) and passwordGET /logout β redirects to /home| Route | Access |
|---|---|
/api/auth/** |
Public |
GET /api/products/**, GET /api/categories/** |
Public |
/actuator/health |
Public |
/api/cart/**, /api/orders/**, /api/addresses/** |
ROLE_CUSTOMER |
POST/PUT/DELETE /api/products/** |
ROLE_ADMIN |
POST/PUT/DELETE /api/categories/** |
ROLE_ADMIN |
/api/orders/admin/** |
ROLE_ADMIN |
/actuator/** |
ROLE_ADMIN |
/api/users/** |
Authenticated |
Token format: Authorization: Bearer <jwt_token>
Token expiry: 24 hours
Password encoding: BCrypt (strength 12)
| URL | Page | Auth |
|---|---|---|
/home |
Product catalog β browse, search, filter by category | No |
/products/{id} |
Product detail with size variants | No |
/login |
Login form (email or phone) | No |
/register |
Registration form with live validation | No |
/forgot-password |
Request password reset via email | No |
/cart |
Shopping cart | Yes |
/orders |
Order history | Yes |
/orders/place |
Place order β choose delivery address | Yes |
/addresses/add |
Add new delivery address | Yes |
/admin/dashboard |
Admin dashboard β users, products, orders | ADMIN |
Each pickle product has 3 size variants:
| Size | Label | Weight |
|---|---|---|
SMALL |
Β½ kg | 500g |
MEDIUM |
1 kg | 1000g |
LARGE |
2 kg | 2000g |
Products are displayed grouped by name on the home and detail pages.
| Condition | Charge |
|---|---|
| Order total β₯ βΉ1000 | FREE |
| First order β₯ βΉ500 | FREE |
| All other orders | βΉ70 flat |
deliveryCharge = βΉ0 [if orderTotal β₯ βΉ1000 OR first order β₯ βΉ500]
deliveryCharge = βΉ70 [all other orders]
grandTotal = totalAmount + deliveryCharge
/api/auth| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
Public | Register new customer |
| POST | /api/auth/login |
Public | Login with email or phone, returns JWT |
| POST | /api/auth/forgot-password |
Public | Send password reset OTP to email |
| POST | /api/auth/reset-password |
Public | Reset password using OTP token |
| GET | /api/auth/verify/{token} |
Public | Email verification on registration |
Register Request:
{
"username": "Ravi Raju",
"email": "ravi@example.com",
"password": "yourpassword",
"phoneNumber": "9876543210"
}
Password must be at least 8 characters and contain at least one number and one special character.
Login Request:
{
"email": "ravi@example.com",
"password": "yourpassword"
}
Can also login with phone number instead of email.
Login Response:
{
"success": true,
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"email": "ravi@example.com",
"username": "Ravi Raju",
"role": "ROLE_CUSTOMER"
}
}
/api/users| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/users/{id} |
Authenticated | Get user by ID |
| GET | /api/users/email/{email} |
Authenticated | Get user by email |
| PUT | /api/users/{id} |
Authenticated | Update user details |
| DELETE | /api/users/{id} |
Authenticated | Delete user account |
/api/categories| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/categories |
Public | Get all categories |
| GET | /api/categories/{id} |
Public | Get category by ID |
| POST | /api/categories |
ADMIN | Add new category |
| PUT | /api/categories/{id} |
ADMIN | Update category |
| DELETE | /api/categories/{id} |
ADMIN | Delete category |
/api/products| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/products?page=0&size=10&sort=price,asc |
Public | All products (paginated) |
| GET | /api/products/{id} |
Public | Single product |
| GET | /api/products/category/{categoryId} |
Public | By category (paginated) |
| GET | /api/products/search?name=mango |
Public | Search by name |
| GET | /api/products/grouped |
Public | All products grouped by name with size variants |
| GET | /api/products/grouped/category/{categoryId} |
Public | Grouped by category |
| GET | /api/products/grouped/search?keyword=chicken |
Public | Grouped search |
| POST | /api/products |
ADMIN | Add product |
| PUT | /api/products/{id} |
ADMIN | Update product |
| DELETE | /api/products/{id} |
ADMIN | Delete product |
Grouped Product Response:
{
"name": "Mango",
"description": "Traditional Andhra Avakaya",
"categoryName": "Veg Pickles",
"categoryId": 1,
"variants": [
{ "id": 1, "size": "SMALL", "sizeLabel": "Β½ kg", "price": 149.00, "inStock": true, "quantity": 20 },
{ "id": 2, "size": "MEDIUM", "sizeLabel": "1 kg", "price": 249.00, "inStock": true, "quantity": 15 },
{ "id": 3, "size": "LARGE", "sizeLabel": "2 kg", "price": 449.00, "inStock": false, "quantity": 0 }
]
}
/api/cart| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/cart/user/{userId} |
CUSTOMER | Get cart with items and total |
| POST | /api/cart/user/{userId}/product/{productId}?quantity=2 |
CUSTOMER | Add item |
| PUT | /api/cart/item/{cartItemId}?quantity=3 |
CUSTOMER | Update quantity |
| DELETE | /api/cart/item/{cartItemId} |
CUSTOMER | Remove item |
| DELETE | /api/cart/user/{userId}/clear |
CUSTOMER | Clear cart |
Adding an existing product to the cart merges the quantity rather than duplicating it. Adding out-of-stock products is blocked with a clear error message.
/api/orders| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/orders/user/{userId} |
CUSTOMER | Get my orders |
| GET | /api/orders/{id} |
CUSTOMER | Get order (JWT-verified ownership) |
| POST | /api/orders |
CUSTOMER | Place COD order |
| DELETE | /api/orders/{id} |
CUSTOMER | Cancel order |
Place Order Request:
{
"addressId": 1
}
userIdis read from the JWT token β not the request body. This prevents placing orders under another userβs ID.
Order Response:
{
"id": 12,
"status": "CONFIRMED",
"totalAmount": 398.00,
"deliveryCharge": 70.00,
"grandTotal": 468.00,
"orderDate": "2025-01-15T10:30:00",
"deliveryAddress": "12 Main St, Kurnool, Kurnool - 518001",
"items": [
{
"productId": 5,
"productName": "Ginger Pickle",
"quantity": 2,
"sizeLabel": "1 kg",
"priceAtTimeOfOrder": 199.00,
"itemTotal": 398.00
}
]
}
Cancellation rule: Only CONFIRMED orders can be cancelled. Stock is automatically restored.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/orders/admin/all?page=0&size=10 |
ADMIN | All orders (paginated) |
| GET | /api/orders/admin/{id} |
ADMIN | Any order by ID |
| PUT | /api/orders/admin/{id}/status?status=SHIPPED |
ADMIN | Update status |
Order statuses: PENDING β CONFIRMED β SHIPPED β DELIVERED / CANCELLED
/api/addresses| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/addresses/user/{userId} |
CUSTOMER | Get all addresses |
| GET | /api/addresses/{id} |
CUSTOMER | Get address by ID |
| POST | /api/addresses/user/{userId} |
CUSTOMER | Add address |
| PUT | /api/addresses/{id} |
CUSTOMER | Update address |
| DELETE | /api/addresses/{userId}/{id} |
CUSTOMER | Delete address |
Address Request:
{
"name": "Home",
"street": "12 Market Road",
"city": "Kurnool",
"district": "Kurnool",
"state": "Andhra Pradesh",
"pincode": "518001"
}
pincodemust be exactly 6 digits, cannot start with 0.
| Endpoint | Access |
|---|---|
/actuator/health |
Public |
/actuator/info |
Public |
/actuator/** |
ADMIN only |
| Table | Description |
|---|---|
users |
Customer and admin accounts |
roles |
ROLE_CUSTOMER, ROLE_ADMIN |
user_roles |
Many-to-many join |
categories |
Veg / Non-Veg |
products |
All variants (name + size + stock) |
carts |
One cart per user |
cart_items |
Items with quantity |
orders |
Orders with total, delivery charge, status |
order_items |
Line items with price snapshot at order time |
addresses |
Delivery addresses |
password_reset_tokens |
Secure tokens for forgot-password flow |
email_verification_tokens |
Tokens for email verification on registration |
All REST errors return a consistent format:
{
"success": false,
"message": "Product not found with id: 99",
"data": null
}
| Scenario | HTTP Status |
|---|---|
| Resource not found | 404 |
| Duplicate email on register | 400 |
| Empty cart on order | 400 |
| Out of stock on add to cart | 400 |
| Cancelling non-CONFIRMED order | 400 |
| Insufficient stock | 400 |
| Validation errors | 400 |
| Not authenticated | 401 |
| Wrong role | 403 |
POST https://ammapickles-ecommerce.onrender.com/api/auth/register
Content-Type: application/json
{
"username": "Test User",
"email": "test@example.com",
"password": "test@1234",
"phoneNumber": "9876543210"
}
POST https://ammapickles-ecommerce.onrender.com/api/auth/login
Content-Type: application/json
{
"email": "test@example.com",
"password": "test@1234"
}
Authorization tab β Bearer Token β paste the token.
GET https://ammapickles-ecommerce.onrender.com/api/products/grouped
POST https://ammapickles-ecommerce.onrender.com/api/cart/user/1/product/5?quantity=2
Authorization: Bearer <token>
POST https://ammapickles-ecommerce.onrender.com/api/addresses/user/1
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Home",
"street": "12 Main Road",
"city": "Kurnool",
"district": "Kurnool",
"state": "Andhra Pradesh",
"pincode": "518001"
}
POST https://ammapickles-ecommerce.onrender.com/api/orders
Authorization: Bearer <token>
Content-Type: application/json
{
"addressId": 1
}
| Feature | Description | Status |
|---|---|---|
| Product Image Support | Add imageUrl field to products so each pickle displays its own photo | π Planned |
| Razorpay Payment | Online payment integration | π Planned |
| Order Detail Page (Web) | Individual order detail page showing full breakdown | π Planned |
| Interactive Orders Page | Expandable timeline panel with order status tracking | π Planned |
Ravi Raju Chintalapudi Java Backend Developer π GitHub: @reachraviraju
Built with β€οΈ and spice β straight from Andhra.