Skip to content

GET/en/projects/travel-agency

Travel agency management system

Search for a trip, book it for several people, pay with PayPal and download the ticket: the management system of a small travel agency, built with Spring Boot.

stack
Java 25, Spring Boot 3.5, Spring Security, Spring Data JPA, MariaDB
payments
PayPal, tested end to end in the sandbox
code
private repository
status
Completed

Starting point

My first project was a library in plain Java, with the books in a text file. The travel agency keeps the same split into entities and services, in a different domain.

The goal was to go the whole way with a Spring application: from in-memory data to a relational database, from the console to the web, through to payment and the ticket. The choices and the backend are mine; the HTML pages were generated by AI.

Persistence, one step at a time

After the library I got to JPA one step at a time, each step fixing a problem of the one before.

  1. May 2025 another project The library plain Java, books in a text file where it started
  2. 16 October 2025 In-memory lists trips and bookings in in-memory lists lost on restart
  3. 24 October 2025 Hand-written JDBC SQL in the code, each service opens its own connection data survives a restart
  4. 29 October 2025 DataSource and HikariCP Spring injects a pooled DataSource connections returned to the pool and reused
  5. 1 December 2025 JPA and Spring Data entities and repositories the repeated JDBC code goes away

MariaDB the same database, a different way in

How it is built

The console came first, with the rules already in the services. The web was added on top of the same services, in the same process, without touching the booking logic.

  • Console the original interface
    Menus no rules
  • Browser Thymeleaf and JavaScript
    Spring Security form login, session
    Controllers thin
services rules and transactions; only DTOs go up
  • users
  • trips
  • bookings
  • tickets
  • payments
  • email
  • tokens
Repositories Spring Data JPA
HikariCP → MariaDB one database for the console and the web

the services also talk to

  • PayPal REST API
  • Gmail email

The console menu and the PayPal capture create a booking through the same service method.

From search to ticket

The order and the capture go through the server, with its credentials, and on the web the booking is created only when PayPal tells the server the payment is complete.

  1. browser → server search destination, price and seats, in one query with optional filters
  2. browser → server confirmation price, discount, number of people
  3. server → PayPal creates the order with the server’s credentials
  4. browser → PayPal the customer approves in the PayPal popup
  5. server → PayPal capture → COMPLETED requested by the browser
  6. server → MariaDB booking and seats a single transaction
  7. browser → server the ticket PDF with a QR code, only if the booking is yours

The data model

Only the columns that reflect a choice: seats as a counter on the trip, the price frozen in the booking, the token that expires and works once.

users

  • email
  • password hash only
  • enabled after email confirmation

booked_trips

  • people_count people
  • final_price total paid, frozen
  • applied_discount discount at the time, frozen

trips

  • price today’s price
  • discount_percentage today’s discount
  • available_seats seat counter

and around users

followed_trips

  • user_id
  • trip_id

confirmation_token

  • token random
  • expires_at short expiry
  • confirmed_at works once
  • token_type confirmation or reset

Decisions

  • ADR 001 Accepted

    A session, not a token

    context
    The HTML pages are rendered by the same server with Thymeleaf: there is no separate frontend.
    decision
    Login through Spring Security’s form and a server-side session, not a JWT.
    consequences
    Spring Security handles login, logout and redirects with no extra code, and logging out really ends the session. In the driving school system, with a separate client, I used a JWT instead.
  • ADR 002 Accepted

    The price is frozen in the booking

    context
    A trip’s price and discount can change after a purchase. Seats are not assigned: all that matters is how many are left.
    decision
    The booking stores the total paid, the discount applied at that moment and the number of people. The trip keeps a counter of free seats.
    consequences
    The history stays what the customer paid, even if the agency changes the discount later. One row per seat wasn’t needed: with no assigned seats, a counter is enough.
  • ADR 003 Accepted

    A database token for confirmation and reset

    context
    Two moments need proof that an email belongs to whoever is using it: sign-up and a forgotten password.
    decision
    One table: a random token that expires soon and works only once, with a type, confirmation or reset. The account stays disabled until the email is confirmed.
    consequences
    The token is invalidated by writing a date, which a signed stateless token can’t do without a revocation list. The type stops a confirmation link from changing a password, and password reset came later as one more type, not another table.

Passwords

  • From plain text to a hash

    At first passwords were stored in plain text, and login compared them as they were. Then I studied how hashing works and what BCrypt does, and switched to it.

    before
    plain text stored and compared as typed
    after
    BCrypt only the hash in the database, checked at login

Status

Complete, with PayPal in the sandbox. The next project was the driving school management system.

What I learned

Writing JDBC, connection handling and row-to-object mapping by hand before moving to JPA showed me what an ORM does for me, and where to look when it does something I don’t expect. With PayPal I saw that a payment isn’t a single call but an order that goes through several states, and that the booking must come only from a confirmed capture.

If I built it again, I would hold the seats before asking for the payment: today they are taken after the capture, and if they run out in between, the customer has paid for a booking that never gets created.