DEV Community

Ayush Shrivastava
Ayush Shrivastava

Posted on

Building an AI-Powered Travel Planner with Spring AI and Azure OpenAI

A complete step-by-step tutorial for building a production-ready REST API that generates personalized travel itineraries using AI.


Table of Contents

  1. What We're Building
  2. Project Setup & Dependencies
  3. The Application Entry Point
  4. Request & Response Models (DTOs)
  5. Prompt Engineering with Spring AI
  6. The Service Layer — Core Business Logic
  7. The Controller Layer — REST API
  8. Exception Handling & Error Responses
  9. OpenAPI / Swagger Configuration
  10. Application Configuration
  11. Running the Application
  12. Testing the API
  13. Real-World Use Cases
  14. Troubleshooting Common Issues

1. What We're Building

Imagine you're planning a trip to Goa. You have ₹25,000, 5 days, and you love beaches, food, and nightlife. Wouldn't it be great if an AI could instantly generate a complete day-by-day itinerary — including activities, meal suggestions, budget breakdown, packing checklists, and safety tips?

That's exactly what this project does.

The AI Travel Planner is a Spring Boot REST API that:

  • Accepts your travel preferences (destination, days, budget, style, interests)
  • Sends them to Azure OpenAI (GPT model)
  • Returns a structured, realistic travel plan as JSON

Real-world example:

A user planning a graduation trip with friends can send a request for "Goa, 5 days, ₹25,000, Budget style, interested in Beaches/Food/Nightlife" and get back a complete itinerary including scooter rental advice, beach shack recommendations, and safety tips — all within seconds.


2. Project Setup & Dependencies

Let's start with the foundation — the pom.xml file. Think of this as the recipe card for your project; it tells Maven what ingredients (libraries) you need.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.16</version>
</parent>
Enter fullscreen mode Exit fullscreen mode

Key Dependencies Explained:

Dependency Purpose Real-World Analogy
spring-boot-starter-web Builds REST APIs Like having a restaurant kitchen — gives you all the tools to serve HTTP requests
spring-boot-starter-validation Validates user input Like a bouncer checking IDs at the door — ensures only valid data enters your system
spring-ai-starter-model-azure-openai Connects to Azure OpenAI Like having a direct phone line to a genius travel agent who knows everything
springdoc-openapi-starter-webmvc-ui Auto-generates API docs Like a menu board showing all available dishes with descriptions
lombok Reduces boilerplate code Like a personal assistant who writes your repetitive code for you
spring-boot-starter-actuator Health monitoring Like a car's dashboard — shows you engine status, fuel level, etc.

Real-world scenario:

In a real startup, you might swap Azure OpenAI for OpenAI directly, or add Spring Security for authentication. The modular dependency design lets you swap components without rewriting code.


3. The Application Entry Point

@SpringBootApplication
public class SpringAiTravelPlannerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAiTravelPlannerApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the ignition switch of your application. @SpringBootApplication is a convenience annotation that combines three things:

  • @Configuration — Marks this as a source of bean definitions
  • @EnableAutoConfiguration — Tells Spring to automatically configure components based on dependencies
  • @ComponentScan — Tells Spring to scan this package and sub-packages for components

Real-world analogy:

Think of @SpringBootApplication like turning the key in your car ignition. One action triggers a cascade: fuel pumps activate, the starter motor turns, engine cylinders fire, and your car comes to life. Similarly, this one annotation triggers Spring to scan, configure, and bootstrap your entire application.


4. Request & Response Models (DTOs)

DTOs (Data Transfer Objects) are plain Java classes that carry data between the client and server. They're like order forms and invoices in a restaurant.

4.1 TravelPlanRequest — The Order Form

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TravelPlanRequest {
    @NotBlank(message = "Destination cannot be blank")
    private String destination;

    @Min(value = 1, message = "Days must be between 1 and 30")
    @Max(value = 30, message = "Days must be between 1 and 30")
    private int days;

    @Positive(message = "Budget must be greater than 0")
    private double budget;

    private String travelStyle;

    @NotEmpty(message = "Interests cannot be empty")
    private List<String> interests;
}
Enter fullscreen mode Exit fullscreen mode

Annotation explanations with real-world examples:

  • @NotBlank — "Destination cannot be blank"

    Imagine booking a flight without specifying where you're going. The airline wouldn't know where to send you!

  • @Min / @Max — Trip duration between 1 and 30 days

    A 0-day trip or a 100-day trip might not make practical sense. These constraints keep data realistic.

  • @Positive — Budget must be > 0

    You can't travel with negative money (unfortunately!).

  • Lombok @Data generates getters, setters, toString, equals, and hashCode automatically

    Without Lombok, you'd write ~50 lines of boilerplate code. With Lombok, just one annotation.

Real-world validation scenarios:

A travel booking website like MakeMyTrip uses similar validation: you can't search for a "0-day trip" or a "negative budget". These validations catch errors before they reach the database or AI service.

4.2 TravelPlanResponse — The Invoice

The response model is more complex because the AI returns a rich, structured travel plan. It contains nested classes:

public class TravelPlanResponse {
    private String destination;
    private String tripOverview;
    private List<DayItinerary> itinerary;
    private EstimatedBudget estimatedBudget;
    private List<String> recommendedPlaces;
    private List<String> food;
    private String transportation;
    private List<String> packingChecklist;
    private List<String> travelTips;
    private List<String> safetyTips;
}
Enter fullscreen mode Exit fullscreen mode

Nested classes:

  • DayItinerary — Contains day number, title, activities, meals, and accommodation for each day
  • Meals — Breakfast, lunch, and dinner suggestions
  • EstimatedBudget — Total budget with category breakdown (accommodation, food, transport, etc.)

Real-world example of the response:

When you ask an AI travel agent for a "5-day Goa trip," the response includes specific beach names (Baga, Anjuna, Vagator), exact restaurants, scooter rental prices (₹300-500/day), and even packing items like "waterproof phone pouch" — all generated dynamically by AI.


5. Prompt Engineering with Spring AI

Prompt engineering is the art of crafting instructions for AI models. This is where the magic happens.

5.1 The Prompt Template File

In src/main/resources/prompts/travel-plan-prompt.st:

Generate a detailed travel plan for {destination} for {days} days with a budget of ₹{budget}.

Travel Style: {travelStyle}
Interests: {interests}

Provide the response as a JSON object with the following structure:
{
  "tripOverview": "...",
  "itinerary": [...],
  "estimatedBudget": {...},
  ...
}
Enter fullscreen mode Exit fullscreen mode

The {variable} syntax is from Spring AI's PromptTemplate. Variables get replaced at runtime with actual values.

Why this approach is powerful:

Instead of hardcoding prompts in Java code, you keep them in separate .st files. This means non-developers (like content writers or domain experts) can tweak prompts without touching code — a real productivity win in production teams.

5.2 PromptTemplateConfig — Wiring the Template

@Configuration
public class PromptTemplateConfig {
    @Bean
    public Resource travelPlanPromptResource() {
        return new ClassPathResource("prompts/travel-plan-prompt.st");
    }
}
Enter fullscreen mode Exit fullscreen mode

This creates a Spring bean that loads the prompt template file from the classpath. The @Bean annotation makes this Resource available for dependency injection elsewhere.

Real-world analogy:

Think of this like registering a template document in your company's document management system. Anyone who needs the template can request it from the system instead of searching through folders.

5.3 TravelPromptBuilder — Building Prompts

@Component
public class TravelPromptBuilder {

    private static final String SYSTEM_PROMPT_TEXT = """
            You are an experienced travel planner. Generate accurate, realistic,
            beginner-friendly travel itineraries with practical recommendations.
            Always provide structured responses, estimated budgets, local travel
            advice, and safety tips. Never invent nonexistent tourist attractions.
            """;

    private final Resource promptTemplateResource;

    public Prompt buildTravelPlanPrompt(TravelPlanRequest request) {
        SystemMessage systemMessage = new SystemMessage(SYSTEM_PROMPT_TEXT);
        PromptTemplate promptTemplate = new PromptTemplate(promptTemplateResource);

        Map<String, Object> variables = Map.of(
            "destination", request.getDestination(),
            "days", String.valueOf(request.getDays()),
            "budget", String.valueOf((int) request.getBudget()),
            "travelStyle", request.getTravelStyle() != null
                ? request.getTravelStyle() : "Standard",
            "interests", String.join(", ", request.getInterests())
        );

        Message userMessage = promptTemplate.createMessage(variables);
        return new Prompt(List.of(systemMessage, userMessage));
    }
}
Enter fullscreen mode Exit fullscreen mode

Breaking down the prompt structure:

Component Purpose Real-World Analogy
System Message Sets the AI's role and behavior Like briefing a travel agent before they talk to a client: "You specialize in budget travel, always recommend local food, never make up attractions"
User Message The actual request with variables Like the client saying: "I want to go to Goa for 5 days with ₹25,000, I love beaches and food"
PromptTemplate Fills variables into a template Like a mail-merge — same letter structure, personalized details

Real-world scenario:

A travel tech startup could A/B test different system prompts to see which generates more bookings. By keeping prompts in files, non-engineers can run experiments without deployment cycles.


6. The Service Layer — Core Business Logic

The TravelPlanService is the brain of the application. It orchestrates the entire flow.

@Service
public class TravelPlanService {

    private final ChatClient chatClient;
    private final TravelPromptBuilder promptBuilder;
    private final ObjectMapper objectMapper;

    public TravelPlanService(ChatClient.Builder chatClientBuilder,
                              TravelPromptBuilder promptBuilder,
                              ObjectMapper objectMapper) {
        this.chatClient = chatClientBuilder
            .defaultSystem("You are an experienced travel planner...")
            .build();
        this.promptBuilder = promptBuilder;
        this.objectMapper = objectMapper;
    }
Enter fullscreen mode Exit fullscreen mode

Constructor Injection

Spring automatically provides the ChatClient.Builder (configured via application.yml), our TravelPromptBuilder, and Jackson's ObjectMapper. This is dependency injection — Spring wires everything together.

Real-world analogy:

Imagine a restaurant where the chef doesn't grow their own vegetables or make their own plates. The ingredients arrive from suppliers (Spring) already prepped. The chef just focuses on cooking.

The Generate Method — Step by Step

public TravelPlanResponse generateTravelPlan(TravelPlanRequest request) {
    // Step 1: Build the prompt from user request
    Prompt prompt = promptBuilder.buildTravelPlanPrompt(request);

    // Step 2: Send to Azure OpenAI and get response
    String response;
    try {
        response = chatClient.prompt(prompt).call().content();
    } catch (Exception ex) {
        String errorMsg = extractErrorMessage(ex);
        throw new AiServiceException("Failed to generate travel plan: " + errorMsg, ex);
    }

    // Step 3: Validate response isn't empty
    if (response == null || response.isBlank()) {
        throw new AiServiceException("AI returned an empty response");
    }

    // Step 4: Parse JSON into Java object
    TravelPlanResponse travelPlan = parseResponse(response, request.getDestination());
    return travelPlan;
}
Enter fullscreen mode Exit fullscreen mode

Step-by-step with a real-world example:

  1. User sends: {destination: "Goa", days: 5, budget: 25000, travelStyle: "Budget", interests: ["Beaches", "Food", "Nightlife"]}
  2. Prompt built: System message + "Generate a detailed travel plan for Goa for 5 days with a budget of ₹25000..."
  3. AI responds: A 2000-word JSON document with 5-day itinerary, budget breakdown, safety tips, etc.
  4. JSON parsed: The raw string is converted into a TravelPlanResponse Java object
  5. Response sent: The formatted travel plan is returned as HTTP JSON response

The extractJson Method — Handling AI Output

private String extractJson(String text) {
    int start = text.indexOf('{');
    int end = text.lastIndexOf('}');
    return text.substring(start, end + 1);
}
Enter fullscreen mode Exit fullscreen mode

Why this is needed:

AI models sometimes wrap JSON in markdown code blocks (

json ...

) or add explanatory text before/after. This method strips everything except the JSON structure — like panning for gold and keeping only the nuggets.

Real-world example:

Without this method, the AI might return:

Here's your travel plan!


json
{"destination": "Goa", ...}

Hope you enjoy!

The extractJson method cleans this into valid JSON.

The extractErrorMessage Method — User-Friendly Errors

private String extractErrorMessage(Exception ex) {
    String message = ex.getMessage();
    if (message != null) {
        if (message.contains("rate limit") || message.contains("RateLimit")) {
            return "Rate limit exceeded. Please try again later.";
        }
        if (message.contains("timeout") || message.contains("Timeout")) {
            return "Request timed out. Please try again.";
        }
        if (message.contains("401") || message.contains("403")) {
            return "Authentication failed. Check API credentials.";
        }
        if (message.contains("429")) {
            return "Too many requests. Please slow down.";
        }
    }
    return "AI service is currently unavailable. Please try again later.";
}
Enter fullscreen mode Exit fullscreen mode

This translates cryptic Azure error messages into human-readable messages. Instead of exposing stack traces to users, they get clear, actionable feedback.


7. The Controller Layer — REST API

The controller is the front door of your application — it receives HTTP requests and sends back responses.

@RestController
@RequestMapping("/api/v1")
@Tag(name = "Travel Plan", description = "AI-powered travel plan generation endpoints")
public class TravelPlanController {
Enter fullscreen mode Exit fullscreen mode
  • @RestController — Combines @Controller and @ResponseBody; every method returns JSON automatically
  • @RequestMapping("/api/v1") — All endpoints start with /api/v1
  • @Tag — For Swagger documentation grouping

The Endpoint

@PostMapping("/travel-plan")
public ResponseEntity<TravelPlanResponse> generateTravelPlan(
        @Valid @RequestBody TravelPlanRequest request) {

    Instant start = Instant.now();
    log.info("Received travel plan request - destination: {}, days: {}, budget: {}",
            request.getDestination(), request.getDays(), request.getBudget());

    TravelPlanResponse response = travelPlanService.generateTravelPlan(request);

    long duration = Duration.between(start, Instant.now()).toMillis();
    log.info("Travel plan request completed in {} ms for {}", duration, request.getDestination());

    return ResponseEntity.ok(response);
}
Enter fullscreen mode Exit fullscreen mode

Key annotations:

Annotation Purpose Real-World Analogy
@PostMapping Maps HTTP POST requests to this method Like a specific mailbox labeled "New Travel Plan Requests"
@Valid Triggers validation before method executes Like a security check before entering a building
@RequestBody Maps HTTP request body to Java object Like an automated form-filler
ResponseEntity Wraps response with status code and headers Like sending a package with proper shipping label

Real-world API call:

POST http://localhost:8080/api/v1/travel-plan
Content-Type: application/json

{
  "destination": "Goa",
  "days": 5,
  "budget": 25000,
  "travelStyle": "Budget",
  "interests": ["Beaches", "Food", "Nightlife"]
}
Enter fullscreen mode Exit fullscreen mode

The controller also includes detailed OpenAPI annotations (@Operation, @ApiResponses, @ExampleObject) that auto-generate beautiful Swagger documentation — making it easy for frontend developers to understand and test the API.


8. Exception Handling & Error Responses

This project uses Spring's @RestControllerAdvice for centralized exception handling — one place to handle all errors.

@RestControllerAdvice
public class GlobalExceptionHandler {
Enter fullscreen mode Exit fullscreen mode

ProblemDetail — Modern Error Responses

Spring Boot 3 introduced ProblemDetail (RFC 9457) for standardized error responses.

Validation errors (400 Bad Request):

{
  "type": "urn:problem-type:validation",
  "title": "Validation Failed",
  "status": 400,
  "detail": "destination: Destination cannot be blank",
  "timestamp": "2026-07-21T12:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

AI service errors (503 Service Unavailable):

{
  "type": "urn:problem-type:ai-service",
  "title": "AI Service Error",
  "status": 503,
  "detail": "Failed to generate travel plan: Rate limit exceeded...",
  "timestamp": "2026-07-21T12:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Why centralized error handling matters:

Imagine 10 different controllers each handling errors differently. Some return 400, some return "bad request", some return cryptic stack traces. @RestControllerAdvice ensures every error response follows the same format — critical for frontend developers building error-handling logic.

Real-world analogy:

Think of this like a mall's customer service desk. Instead of each store handling complaints differently, there's one central desk with standard procedures. Customers know exactly where to go and what format to expect.


9. OpenAPI / Swagger Configuration

The OpenApiConfig class customizes the auto-generated API documentation:

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI travelPlannerOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("AI Travel Planner API")
                .description("REST API for generating personalized AI-powered travel plans")
                .version("1.0.0")
                .contact(new Contact()
                    .name("Developer")
                    .email("dev@example.com"))
                .license(new License()
                    .name("MIT")))
            .servers(List.of(
                new Server()
                    .url("http://localhost:" + serverPort)
                    .description("Local development server")
            ));
    }
}
Enter fullscreen mode Exit fullscreen mode

What this provides:

  • A /swagger-ui.html page where you can test all endpoints interactively
  • Auto-generated documentation based on your controller annotations
  • Example requests and responses (from @ExampleObject annotations in the controller)

Real-world use:

In a team of 5 developers, the frontend mobile team can open Swagger UI, see the exact request format expected, test with real data, and download the OpenAPI spec to auto-generate their API client code — all without waiting for the backend team to write documentation.


10. Application Configuration

10.1 Main Configuration (application.yml)

spring:
  ai:
    azure:
      openai:
        api-key: ${AZURE_OPENAI_API_KEY}
        endpoint: ${AZURE_OPENAI_ENDPOINT}
        chat:
          options:
            deployment-name: ${AZURE_OPENAI_DEPLOYMENT:gpt-4o}
            temperature: 1.0
Enter fullscreen mode Exit fullscreen mode

Key settings explained:

Setting Purpose Why It Matters
api-key Your Azure OpenAI API key Without this, Azure won't authenticate your requests
endpoint Your Azure OpenAI service URL Tells Spring which Azure resource to call
deployment-name Which model deployment to use Different models (GPT-4o, GPT-4, GPT-35-turbo) have different capabilities and costs
temperature: 1.0 Controls AI creativity (0.0-1.0) Lower = more deterministic; Higher = more creative. Some models only support specific values

Real-world scenario:

In production, you'd never hardcode these values. Using ${AZURE_OPENAI_API_KEY} reads from environment variables, which can be set in:

  • Your local terminal for development
  • GitHub Actions secrets for CI/CD
  • Azure Key Vault for production
  • Docker Compose environment variables

10.2 Dev Profile (application-dev.yml)

logging:
  level:
    com.aytravelplanner: DEBUG
    org.springframework.ai: DEBUG
    org.springframework.web: DEBUG

spring:
  devtools:
    restart:
      enabled: true
Enter fullscreen mode Exit fullscreen mode
  • DEBUG logging shows detailed logs about AI prompts and responses — invaluable during development
  • DevTools restart automatically restarts the app when code changes — like hot-reload for Spring Boot

11. Running the Application

Step 1: Set Environment Variables

# Windows PowerShell
$env:AZURE_OPENAI_API_KEY="your-key-here"
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
$env:AZURE_OPENAI_DEPLOYMENT="gpt-4o"
Enter fullscreen mode Exit fullscreen mode

Important: Keep your API key secret! Never commit it to Git. The .env.example file shows what variables are needed without exposing real values.

Step 2: Run the Application

mvn spring-boot:run -Dspring-boot.run.profiles=dev
Enter fullscreen mode Exit fullscreen mode

The dev profile enables detailed logging and hot-reload.

Step 3: Verify It's Running


12. Testing the API

Using cURL

curl -X 'POST' \
  'http://localhost:8080/api/v1/travel-plan' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "destination": "Goa",
  "days": 5,
  "budget": 25000,
  "travelStyle": "Budget",
  "interests": ["Beaches", "Food", "Nightlife"]
}'
Enter fullscreen mode Exit fullscreen mode

Using Swagger UI

  1. Open http://localhost:8080/swagger-ui.html
  2. Click on the "Travel Plan" endpoint
  3. Click "Try it out"
  4. Paste the JSON request body
  5. Click "Execute" — the response appears below with the full travel plan

Sample Response Snippet

{
  "destination": "Goa",
  "tripOverview": "5-day budget beach-and-food-focused trip...",
  "itinerary": [
    {
      "day": 1,
      "title": "Arrival, Settle in & North Goa Beach Evening",
      "activities": [
        "Arrive in Goa. Check in to your budget guesthouse.",
        "Rent a scooter to explore nearby beaches.",
        "Relax at Calangute/Candolim beach..."
      ]
    }
  ],
  "estimatedBudget": {
    "total": 25000,
    "breakdown": {
      "accommodation": 6000,
      "food": 4000,
      "transportation": 5000,
      "activities": 5000,
      "miscellaneous": 5000
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

13. Real-World Use Cases

Use Case 1: Travel Booking Website

A website like MakeMyTrip or Booking.com could integrate this API to offer "AI-Generated Itineraries" as a premium feature:

  1. User searches for flights to Goa
  2. After booking, API generates a personalized 5-day itinerary
  3. User can modify and save it
  4. Monetization: charge ₹99 per AI-generated itinerary

Use Case 2: Travel Agency Automation

A small travel agency handling 100+ inquiries daily:

  1. Customer calls: "I want a 7-day Kerala trip on ₹50,000"
  2. Agent inputs details into internal tool
  3. API generates professional itinerary in seconds instead of hours
  4. Agent reviews, customizes slightly, and sends to client

Use Case 3: Chatbot Integration

A WhatsApp travel chatbot:

  1. User texts: "Plan a 3-day trip to Jaipur"
  2. Bot calls this API
  3. Returns formatted itinerary in chat
  4. User asks follow-ups: "Change budget to ₹30,000"

Use Case 4: Content Generation

A travel blog uses the API to generate content ideas:

  1. Blog writer inputs: "Goa, 5 days, ₹25,000, Budget"
  2. API generates structured content
  3. Writer uses it as a starting template
  4. Customizes with personal experiences and photos

14. Troubleshooting Common Issues

Error Cause Solution
No static resource swagger-ui.html Wrong Swagger path config Set springdoc.swagger-ui.path: /swagger-ui.html
DeploymentNotFound Wrong model deployment name Set AZURE_OPENAI_DEPLOYMENT to match your Azure deployment
temperature does not support 0.7 Some models only support temperature=1 Set temperature: 1.0 in configuration
401 Authentication failed Invalid API key Check AZURE_OPENAI_API_KEY is correct and not expired
400 Bad Request Missing required fields Ensure destination, days, budget, and interests are provided
AI returns invalid JSON Model hallucination The extractJson method handles this; improve the prompt as needed

Architecture Diagram

┌─────────────┐     HTTP Request      ┌──────────────────┐
│   Client    │ ───────────────────>   │   Controller     │
│ (cURL/App)  │                       │  (REST API)      │
└─────────────┘                       └────────┬─────────┘
                                               │
                                               ▼
                                     ┌──────────────────┐
                                     │    Service       │
                                     │  TravelPlanService│
                                     └────────┬─────────┘
                                               │
                                    ┌──────────┴──────────┐
                                    ▼                     ▼
                           ┌─────────────────┐   ┌─────────────────┐
                           │  PromptBuilder   │   │  ObjectMapper   │
                           │ (Build prompt)   │   │ (Parse JSON)    │
                           └────────┬────────┘   └────────┬────────┘
                                    │                     │
                                    ▼                     │
                           ┌─────────────────┐            │
                           │  ChatClient     │────────────┘
                           │ (Azure OpenAI)  │
                           └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Spring AI simplifies LLM integration — The ChatClient API abstracts away HTTP calls, authentication, and streaming
  2. Prompt engineering is crucial — The quality of output depends entirely on how you structure your prompts
  3. Always validate and parse AI output — AI models don't guarantee valid JSON; always add safety nets
  4. Centralized error handling@RestControllerAdvice keeps your error responses consistent
  5. Externalize configuration — Use environment variables for secrets and profile-specific configs
  6. Document APIs automatically — SpringDoc + OpenAPI annotations generate interactive documentation for free

Next Steps & Enhancements

Want to extend this project? Try:

  • Add caching — Store generated plans in Redis or database to avoid redundant AI calls
  • Add PDF export — Convert the itinerary to a downloadable PDF using iText or Apache POI
  • Add user authentication — Spring Security + JWT for multi-user support
  • Add streaming responses — Use SSE (Server-Sent Events) to show the itinerary as it's being generated
  • Support multiple AI providers — Add OpenAI, Anthropic Claude, or Google Gemini via Spring AI's abstraction
  • Add flight/hotel API integration — Fetch real prices from Amadeus or Google Flights APIs
  • Rate limiting — Protect your API from abuse with bucket4j or Spring Cloud Gateway

Built with Spring Boot 3.5, Spring AI 1.0.8, Java 21, and Azure OpenAI.

Top comments (0)