Taco Cloud by Craig Walls, Spring in Action 6th edition. During the course of this book an author creates a complete web application named Taco Cloud. However, I decided to create an application with similar functionalities, but with focus on the REST architecture. Not long ago acquired this book, so more new functionalities are on the way.
A Taco Cloud is a RESTful backend API for a taco ordering, built with Spring Boot 3.4.5. Users can browse ingredients, build custom tacos, manage a shopping cart, check out with Stripe, and track their orders through a full lifecycle — all secured with JWT authentication and backed by automatic email notifications.
- User registration and login with JWT-based authentication
- Role-based access control (
USER,MANAGER,DEVELOPER) - CRUD operations for Ingredients and Tacos
- Shopping cart — add, update quantity, and remove taco items before checkout
- Order lifecycle with enforced status transitions (
AWAITING_PAYMENT→PREPARING→ON_THE_WAY→DELIVERED, orCANCELED) - Stripe integration — Stripe Customers created on registration, Checkout Sessions created on order placement, and payment tracking (
PENDING,COMPLETED,FAILED) - Stripe webhooks —
checkout.session.completedandcheckout.session.expiredevents automatically update payment and order status - Transactional email notifications — order confirmation and order status update emails sent via Gmail SMTP, decoupled from the request/DB transaction using Spring's
ApplicationEventPublisher - Soft-delete support across all major entities
- Database migrations managed by Liquibase
- Pre-loaded seed data for quick local testing
| Layer | Technology |
|---|---|
| Framework | Spring Boot 3.4.5 |
| Language | Java |
| Security | Spring Security 6 + JJWT 0.12.5 |
| Persistence | Spring Data JPA + Hibernate |
| Database | MySQL 8/9 |
| Migrations | Liquibase 4.29.2 |
| Mapping | MapStruct 1.6.3 |
| Validation | Jakarta Validation 3 |
| Payments | Stripe Java SDK |
Spring Mail (JavaMailSender) + Gmail SMTP |
|
| Frontend | html + Thymeleaf |
| Build | Maven |
- Java 17+
- MySQL 8+ running locally (or via Docker)
- Maven 3.8+
- A Stripe account (test mode is fine) and the Stripe CLI for local webhook forwarding
- A Gmail account with an App Password for sending emails
Create a .env file in the project root (the app loads it automatically via spring.config.import):
# Database
DATABASE_URL=jdbc:mysql://localhost:3306/taco_cloud
MYSQL_USER=your_db_user
MYSQL_PASSWORD=your_db_password
# JWT
JWT_SECRET=your_super_secret_key_at_least_256_bits
JWT_EXPIRATION=86400000
# Stripe
STRIPE_API_KEY=sk_test_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_signing_secret
# Mail (Gmail SMTP)
MAIL_USERNAME=your_gmail_address@gmail.com
MAIL_PASSWORD=your_gmail_app_password
APP_MAIL=your_gmail_address@gmail.comCREATE DATABASE taco_cloud;# Using Maven
mvn spring-boot:run
# Or using the pre-built JAR
java -jar taco-cloud-0_0_1-SNAPSHOT.jarLiquibase will automatically run all migrations on startup, including creating tables and seeding test data.
Since the app runs under a non-root context path, make sure to include it when forwarding events:
stripe listen --forward-to localhost:8080/taco_cloud_api/webhook/stripeCopy the signing secret printed by the CLI into STRIPE_WEBHOOK_SECRET.
http://localhost:8080/taco_cloud_api
All endpoints except /auth/** and /webhook/** require a valid JWT token in the Authorization header:
Authorization: Bearer <your_token>
| Password | Role | |
|---|---|---|
bob@mail |
password |
USER |
alice.developer1@mail.com |
password |
USER, DEVELOPER |
Note: Passwords in the database are BCrypt-hashed. The seed password shown above is a placeholder — update the DB seed data with your actual test password.
Emails matching
*developer<n>@*or*manager<n>@*are automatically granted theDEVELOPERorMANAGERrole on registration.
| Method | Path | Description | Auth Required |
|---|---|---|---|
| POST | /auth/register |
Register a new user (creates a Stripe Customer and an empty cart) | No |
| POST | /auth/login |
Login and receive a JWT token | No |
| Method | Path | Description | Auth Required |
|---|---|---|---|
| GET | /ingredient |
List all ingredients (optionally filter by type) |
Yes |
| GET | /ingredient/{id} |
Get ingredient by ID | Yes |
| POST | /ingredient |
Create a new ingredient | DEVELOPER |
| PATCH | /ingredient/{id} |
Update an ingredient | DEVELOPER |
| DELETE | /ingredient/{id} |
Delete an ingredient | DEVELOPER |
| Method | Path | Description | Auth Required |
|---|---|---|---|
| GET | /taco |
List all tacos | Yes |
| GET | /taco/{id} |
Get taco by ID | Yes |
| POST | /taco |
Create a new taco | DEVELOPER |
| PATCH | /taco/{id} |
Update a taco | DEVELOPER |
| DELETE | /taco/{id} |
Delete a taco | DEVELOPER |
| Method | Path | Description | Auth Required |
|---|---|---|---|
| GET | /cart |
Get the current user's cart | Yes |
| GET | /cart/{userId} |
Get a cart by user ID | DEVELOPER, MANAGER |
| POST | /cart/add_item |
Add a taco to the current user's cart (or increase quantity) | Yes |
| PATCH | /cart/update_quantity/{itemId} |
Update a cart item's quantity (0 removes it) |
Yes |
| DELETE | /cart/remove_item/{itemId} |
Remove an item from the current user's cart | Yes |
| Method | Path | Description | Auth Required |
|---|---|---|---|
| POST | /orders |
Place an order from the current cart and create a Stripe Checkout Session | Yes |
| GET | /orders/last |
Get the current user's most recent order | Yes |
| GET | /orders/{orderId} |
Get order by ID | DEVELOPER, MANAGER |
| GET | /orders/my |
Get the current user's orders (optionally filter by status) |
Yes |
| GET | /orders/user/{userId} |
Get orders for a specific user (optionally filter by status) |
DEVELOPER, MANAGER |
| PATCH | /orders/update/{orderId} |
Update an order's status (enforces allowed transitions) | DEVELOPER, MANAGER |
Order status flow:
AWAITING_PAYMENT → PREPARING → ON_THE_WAY → DELIVERED
↓ ↓ ↓
CANCELED CANCELED CANCELED
| Method | Path | Description | Auth Required |
|---|---|---|---|
| GET | /payments/last |
Get the current user's most recent payment | Yes |
| GET | /payments |
Get the current user's payments (optionally filter by status) |
Yes |
| GET | /payments/order/{orderId} |
Get the payment for a specific order | DEVELOPER, MANAGER |
| GET | /payments/user/{userId} |
Get payments for a specific user (optionally filter by status) |
DEVELOPER, MANAGER |
| GET | /payments/success |
Thymeleaf redirect page after a successful Stripe Checkout | No |
| GET | /payments/cancel |
Thymeleaf redirect page after a canceled Stripe Checkout | No |
| Method | Path | Description | Auth Required |
|---|---|---|---|
| POST | /webhook/stripe |
Receives and verifies Stripe events (checkout.session.completed, checkout.session.expired) |
No |
- On registration, a Stripe Customer is created for the user and stored as
stripeCustomerId. - The user adds tacos to their cart (
/cart/add_item). - Placing an order (
POST /orders) converts the cart into anOrder(AWAITING_PAYMENT), clears the cart, creates aPayment(PENDING), and returns a Stripe Checkout Session URL. - The user completes payment on Stripe's hosted checkout page.
- Stripe sends a webhook event to
/webhook/stripe:checkout.session.completed→Paymentset toCOMPLETED,Orderset toPREPARING, and an order confirmation email is published.checkout.session.expired→Paymentset toFAILED,Orderset toCANCELED, and a status update email is published.
- Emails are sent after the database transaction commits (
@TransactionalEventListener(phase = AFTER_COMMIT)), so a failed email never rolls back an order or payment update.
Transactional emails are sent via Gmail SMTP using Spring's JavaMailSender. Two events are handled:
- Order confirmation — sent when a Stripe payment completes
- Order status update — sent whenever an order's status changes (including cancellation and manual updates by staff)
Sending is decoupled from the triggering transaction: services publish an
OrderCreatedEvent/OrderStatusChangedEventviaApplicationEventPublisher, and a listener picks them upAFTER_COMMITto send the mail, so a slow or failed SMTP call can never block or roll back an order/payment update.
Key tables:
users— account info (name, email, address, phone, Stripe customer ID)roles—USER,MANAGER, andDEVELOPERrolesuser_role— many-to-many join between users and rolesingredients— individual taco ingredientstacos— custom taco creations, linked to ingredients viataco_ingredientcarts— one active cart per usercart_items— taco + quantity line items belonging to a cartorders— customer orders with delivery info and status, linked to items viaorder_itemsorder_items— taco + quantity + subtotal snapshot for a placed orderpayments— Stripe session/customer IDs, amount, and status, linked to a user and an order
src/
└── main/
└── java/ihromovyi/tacocloud/
├── client/ # Stripe API client (MyStripeClient)
├── config/ # Configurations (Security, Stripe, Web)
├── controller/ # REST controllers (Auth, Cart, Ingredient, Taco, Order, Payment, Webhook, ...)
├── dto/ # Request/Response DTOs, event DTOs
├── exception/ # Custom exceptions
├── mapper/ # MapStruct mappers
├── model/ # JPA entities (User, Role, Taco, Cart, CartItem, Order, OrderItem, Payment, ...)
├── repository/ # Spring Data JPA repositories
├── security/ # JWT filter, AuthenticationService, UserDetailsService
├── service/ # Business logic layer (cart, order, payment, mail, ingredient, taco, user)
├── validation/ # Custom validators (e.g. password)
└── webhook/ # Stripe webhook handling
└── resources/
├── application.properties
├── templates/ # home.html, payment-success.html, payment-cancel.html, mail templates
├── static/pictures/
└── db/changelog/ # Liquibase migration files
User has to write a password with at least 1 capital letter, 1 lowercase letter and a special sign, else receives a BAD_REQUEST.

Successful login with JWT token received.

If you have any interesting suggestion on how to make the application more interesting you are welcome to make pull request.




