Welcome to Act One — the small, pragmatic ticketing backend that powers Prarambh Theatre's shows. The following is a working scenario of a product from the viewpoint of the operator: how a ticket request is recorded, how a volunteer validates and issues tickets, how attendees check-in at the door, and how the system composes and delivers beautiful ticket cards by email.
The goal here is to be practical: runnable locally for development, auditable for production, and easy to deploy to Google Cloud Run.
Quick story (the short version)
- Alice (patron) visits the box office and gives her name, phone and email.
- The frontend posts a purchase request to Act One; the backend records ticket row(s) in the DB
with status
TRANSACTION_MADE. - A human (or a payment webhook) validates the transaction — the staff triggers
ISSUED. - When a ticket transitions to
ISSUED, the backend generates an in-memory JPG "ticket card" (no disk writes) and sends an aggregated purchase email with attachments. - At the door, staff scans the QR; the system verifies the ticket's status and marks it
USED.
If you'd rather skip the story, jump to "Run locally" or "API examples" below.
Table of contents
- Overview & architecture
- Quickstart (run locally)
- API examples and curl snippets
- Ticket issuance & email flow (how attachments are generated)
- Admin operations (delete users, purge, resend emails)
- Production deployment (Docker + Cloud Run)
- CI / GitHub Actions (recommended)
- Troubleshooting & logs (HikariCP warning, SMTP TLS)
- Contributing and next steps
Act One is intentionally small and opinionated. Key principles:
- Single-process Spring Boot application (Java 21) with Spring Data JPA and H2 for dev.
- Email templates: simple plaintext messages that include ticket details and attach in-memory
JPG ticket cards generated from a classpath template (
Card.jpg). - Ticket card generation is CPU-bound image drawing (QR code + text); generation is done in bounded parallel threads to avoid overloading the JVM in container environments.
- Email sending is performed with a pluggable
EmailSenderimplementation; SMTP is the default.
Core components (high level)
TicketIssuanceService— creates/persists tickets (status TRANSACTION_MADE) and publishes events when purchases are issued.ManualTransactionService— supports validating/issuing tickets byuserIdortransactionId.TicketCardGenerator— generates JPG bytes of the ticket card using ImageIO + ZXing.TicketPurchaseEmailListener— listens for issued events, generates JPG attachments in parallel, and sends a single purchase-level email with all attachments.SmtpEmailSender— sends mail; has retry logic for transient TLS/handshake errors.
Why JPG and not PNG? JPG attachments are smaller and more widely supported for email clients; the system generates JPG bytes in-memory to avoid filesystem writes in serverless containers.
Prereqs:
- Java 21
- Maven 3.8+
- (optional) Docker for container testing
- Build and run the app locally (dev profile uses H2 and local resources):
# Set Admin Password before build (Powershell Commands)
$env:ACTONE_ADMIN_PASSWORD:"<admin-password>"
# Build
mvn clean package
# Run in dev profile (default)
mvn spring-boot:runDefault base URL: http://localhost:8080
H2 console (dev only): http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:tickets, user sa)
Tip: The test profile bundles a Card.jpg in src/test/resources/static/Card.jpg so tests can
exercise card generation.
Authentication model for admin endpoints: a shared-secret header X-Admin-Password which must
match the ACTONE_ADMIN_PASSWORD environment variable (supply it at runtime; not committed to the repo). This is
intended for internal/dev use only — not a substitute for real auth.
Create a user (admin-only):
curl -X POST 'http://localhost:8080/api/users' \
-H 'Content-Type: application/json' \
-H "X-Admin-Password: $ACTONE_ADMIN_PASSWORD" \
--data '{"fullName":"John Doe","phoneNumber":"9876543210","email":"john@example.com"}'Delete a single user (admin-only):
curl -X DELETE 'http://localhost:8080/api/users/123' \
-H "X-Admin-Password: $ACTONE_ADMIN_PASSWORD"Delete ALL users (admin-only) — careful with this one:
curl --location --request DELETE 'http://localhost:8080/api/users/' \
--header "X-Admin-Password: $ACTONE_ADMIN_PASSWORD"Resend ticket-issue email by full name + transactionId (admin-only). This endpoint will:
- find tickets matching transactionId
- filter by
fullName(case-insensitive) - check all matching tickets are
ISSUEDand then re-generate attachments and send the email
curl --location --request POST 'http://localhost:8080/api/resend-email/by-name-and-transaction?fullName=John%20Doe&transactionId=TXN-1234' \
--header "X-Admin-Password: $ACTONE_ADMIN_PASSWORD"Issue tickets (public-facing; returns the created ticket rows):
curl -X POST 'http://localhost:8080/api/tickets/issue' \
-H 'Content-Type: application/json' \
--data '{"fullName":"Alice", "email":"alice@example.com", "phoneNumber":"9999999999", "ticketCount":2, "ticketAmount":"500.00"}'Check-in a ticket by QR code id (door staff):
curl -X POST 'http://localhost:8080/api/tickets/qrcode/<qrCodeId>/checkin'Lookups: by name, by phone, by email are all admin-protected endpoints under /api/tickets.
When a ticket is issued, the application:
- Persists ticket rows (one per seat) in the
ticketstable with statusTRANSACTION_MADE. - When a manual validation/issue happens, the status is moved to
ISSUED. - The app publishes a
TicketPurchaseIssuedEvent(list of tickets) which theTicketPurchaseEmailListenerconsumes asynchronously AFTER COMMIT. - The listener generates ticket-card JPG attachments in parallel (bounded by
ACTONE_TICKET_CARD_MAX_PARALLEL) usingTicketCardGenerator.generateTicketCardJpegBytes(t). - The attachments are passed to
EmailSender.send(...)which delivers the email.
Important implementation notes:
- All JPG generation is done in-memory (no temp files) to be container-friendly.
- Card template is
Card.jpgunderclasspath:/static/(the repo includes one for tests). - If email is disabled via
ACTONE_EMAIL_ENABLED=false, the system logs and skips sending.
- Purge ALL tickets (admin):
DELETE /api/admin/tickets(header required) - Delete users (admin):
DELETE /api/users/{id}orDELETE /api/users/(purge) - Resend email by name+transaction (admin):
POST /api/resend-email/by-name-and-transaction(see example above)
These endpoints are helpful when you need to correct data or re-send emails if a delivery failed.
The following is a repeat of the short commands, but with an example env-prod.yaml template
shown and additional notes about secrets.
- Build the artifact
mvn clean package- Build Docker image
docker build -t act-one:latest .- Tag
docker tag act-one:latest asia-south1-docker.pkg.dev/<PROJECT_ID>/actone-repo/act-one:latest- Authenticate & push
# login & configure docker helper
gcloud auth login
gcloud config set project <PROJECT_ID>
gcloud auth configure-docker asia-south1-docker.pkg.dev
docker push asia-south1-docker.pkg.dev/<PROJECT_ID>/actone-repo/act-one:latest- Deploy to Cloud Run
# Provide production env vars via a secure file (do not commit secrets)
gcloud run deploy act-one-backend \
--region asia-south1 \
--image asia-south1-docker.pkg.dev/<PROJECT_ID>/actone-repo/act-one:latest \
--env-vars-file env-prod.yamlA simple workflow you can add to automatically build/test/package and push to Artifact Registry:
- Steps:
- Checkout
- Set up JDK 21
- mvn -B -DskipTests package
- Build docker image
- Authenticate to GCP using a service account key stored as
GCP_SA_KEYsecret - gcloud config set project
- gcloud auth configure-docker
- docker push
- gcloud run deploy (optional)
- HikariPool "Thread starvation or clock leap detected" warning
-
Symptom: a log line like
HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m9s...) -
Cause: in serverless/cloud environments the JVM can be paused or experience clock skew (e.g. during hibernation or node scheduling). HikariCP's default housekeeping interval is short and can interpret these pauses as thread starvation.
-
What we changed: in
application-prod.ymlwe setspring.datasource.hikari.housekeeping-period-msto 300000 (5 minutes) and tunedidle-timeout/max-lifetime. This reduces false-positive warnings without changing connection semantics.
- SMTP TLS/SSL handshake errors when sending via external SMTP (e.g. Gmail)
-
Symptom (example log):
javax.net.ssl.SSLHandshakeException: Remote host terminated the handshakeorCould not convert socket to TLSduringmailSender.send(...). -
Cause: transient network issues, remote policy blocking, or TLS version/protocol mismatch. In cloud run these can be intermittent.
-
What we changed:
SmtpEmailSendernow retries transient TLS/handshake failures with exponential backoff (1s, 4s, 9s). This dramatically reduces transient failure surface. If failures persist consider:- validating SMTP settings (host/port/auth/starttls)
- using a transactional provider (SendGrid/Mailgun) that offers an HTTP API (more robust in serverless environments)
- verifying TLS cipher/protocol compatibility between the runtime and remote SMTP host
- Large attachments / slow SMTP sends cause timeouts
- We log attachment sizes and time taken to generate and send. If emails take too long, consider
lowering
ACTONE_TICKET_CARD_MAX_PARALLELor offloading card generation to an external worker (e.g. Cloud Tasks or a dedicated microservice).
- Unit & integration tests exercise ticket generation and email flows; see
src/test/java/.... - There is a convenience runner
BarcodePngSmokeRunner(not a unit test) that can locally generate a card JPG for manual inspection. - The ticket-card template and fonts are intentionally simple. For production, provide a
high-resolution
Card.jpginsrc/main/resources/static/.
- Replace the shared-secret admin header with real authentication (OIDC/JWT) for production.
- Add a resilient email-sending queue with persistent retry tracking and alerting.
- Move sensitive config to Secret Manager and enable Workload Identity for Cloud Run.
- Add an HTML email template (Thymeleaf) for nicer customer-facing emails while keeping a plaintext fallback.
- Set Admin Environment Variable:
$env:ACTONE_ADMIN_PASSWORD="<admin-password>" - Build JAR:
mvn clean package - Run (dev):
mvn spring-boot:run - Docker build:
docker build -t act-one:latest . - Docker tag:
docker tag act-one:latest asia-south1-docker.pkg.dev/<PROJECT>/actone-repo/act-one:latest - Docker push:
docker push asia-south1-docker.pkg.dev/<PROJECT>/actone-repo/act-one:latest - Deploy:
gcloud run deploy act-one-backend --region asia-south1 --image <IMAGE> --env-vars-file env-prod.yaml