AI Prescription Processing Engine
Build a Spring Boot API that uses Google Gemini to analyze drug interactions.
Introduction
30 Second Summary
Every time a pharmacist fills a prescription, they have to check whether the combination of drugs is safe. Mixing the wrong medications can cause serious harm. That mental checklist is slow, error-prone, and impossible to scale.
In this project, you will build MediSynk's Prescription Processing Engine. It is a Spring Boot REST API that uses Google Gemini AI to automatically analyze prescriptions for drug interactions, approve safe ones, and flag dangerous ones for pharmacist review.
What You'll Build
A Spring Boot backend that receives prescriptions, looks up medications in a MySQL database, sends the drug combination to Gemini for interaction analysis, and decides whether to approve or escalate each prescription.
By the end of this project, you'll have:
- A Spring Boot REST API with proper layered architecture (Controller, Service, Repository) managing medications and prescriptions.
- A Spring AI integration with Google Gemini that returns structured interaction reports including severity levels, specific interactions, and pharmacist recommendations.
- An automated prescription workflow that auto-approves safe prescriptions and escalates dangerous drug combinations for pharmacist review.
- Secret Mission: Add an AI-powered medication expiry monitoring endpoint that prioritizes which near-expiry drugs to reorder or discount.
Are there any prerequisites?
You need basic Java knowledge, MySQL installed and running on your machine, and a free Gemini API key from Google AI Studio. You also need JDK 17 or higher, VS Code with the Spring Boot Extension Pack, and Maven.
Before We Start
Before We Start...
Set Up the Spring Boot Project
Every Spring Boot application starts with a project scaffold. You'll generate yours from Spring Initializr, a web tool that bundles the right dependencies into a ready-to-code project.
In this step, you'll generate the MediSynk project, connect it to a MySQL database, and add the Spring AI library that will power the prescription analyzer.
In this step, get ready to:
- Generate a Spring Boot project with Spring Web, Spring Data JPA, and MySQL dependencies.
- Create a MySQL database and configure the application connection.
- Add the Spring AI Google Gemini dependency and verify the app starts.
Generate the project with Spring Initializr
Spring Initializr lets you pick your dependencies and generates a fully configured Maven project as a ZIP file. You'll configure the project metadata and select three core dependencies to start.
- Open start.spring.io in your browser.
- Set Project to Maven.
- Set Language to Java.
- Set Spring Boot to 3.5.11.
- Enter com.medisynk in the Group field.
- Enter medisynk in the Artifact field.
- Enter medisynk in the Name field.
- Enter MediSynk Smart Pharmacy Management System in the Description field.
- Set Package name to com.medisynk.
- Set Packaging to Jar.
- Set Java to 17.
Now add the three starter dependencies that Spring Initializr will bundle into your project.
- Click Add Dependencies on the right side of the page.
- Search for and add Spring Web.
- Search for and add Spring Data JPA.
- Search for and add MySQL Driver.
- Click Generate at the bottom to download the ZIP file.
Why these three dependencies?
Spring Web gives you the tools to build REST API endpoints. Spring Data JPA handles database access through JPA entities and repositories. MySQL Driver lets your app talk to a MySQL database.
You'll add the Spring AI dependency manually next, because Spring Initializr does not list it as an option.
- Extract the downloaded medisynk.zip file to your Desktop.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Select the medisynk folder you just extracted, then click Open.
Spring Initializr does not include the Spring AI starter, so you'll add it manually to pom.xml. This requires two additions: a BOM (Bill of Materials) that manages Spring AI versions, and the dependency itself.
- Open pom.xml in the VS Code Explorer sidebar.
- Find the <properties> section and add the Spring AI version property below the <java.version> line:
<spring-ai.version>1.1.4</spring-ai.version>
- Add this entire <dependencyManagement> section right after the closing </properties> tag:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- Scroll down to the <dependencies> section and add the Google GenAI starter alongside the other dependencies:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
What does a BOM do?
A BOM (Bill of Materials) is a special Maven dependency that manages version numbers for a family of libraries. By importing the Spring AI BOM at version 1.1.4, you don't need to specify a version on each individual Spring AI dependency.
The ${spring-ai.version} syntax references the property you added in <properties>. This keeps the version number in one place, making future upgrades easier.
- Save pom.xml.
Seeing red squiggles in pom.xml?
Make sure the <dependencyManagement> section is placed between </properties> and <dependencies>. Placing it inside another section breaks the XML structure.
Check that every opening tag (like <dependency>) has a matching closing tag (</dependency>).
Help me fix XML errors in my pom.xml
✔️ Awesome, I've got everything!
Make sure you've saved pom.xml before moving on.
ⓧ I'd like to double check the full code
Here is the full pom.xml for comparison. Your file should match this structure.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.11</version>
<relativePath/>
</parent>
<groupId>com.medisynk</groupId>
<artifactId>medisynk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>medisynk</name>
<description>MediSynk Smart Pharmacy Management System</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.4</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Create the MySQL database and configure the application
Spring Boot needs a database to connect to on startup. You'll create an empty MySQL database now and tell Spring Boot where to find it.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Log into MySQL by running this command:
mysql -u root -p
Enter your MySQL root password when prompted. You should see the mysql> prompt.
- Create the MediSynk database by running this SQL command:
CREATE DATABASE medisynk;
- Verify the database was created by running:
SHOW DATABASES;
You should see medisynk in the list of databases.
- Exit MySQL by typing:
EXIT;
Can't connect to MySQL?
Make sure the MySQL service is running. On Windows, check Services (search for it in the Start menu) and look for MySQL. On macOS, try running mysql.server start in your terminal.
If you get Access denied, double check that you are using the correct root password.
Help me troubleshoot my MySQL connection
Now you need to tell Spring Boot how to connect to this database and how to reach the Gemini API.
- In the VS Code Explorer sidebar, expand src/main/resources and open application.properties.
- Replace the entire file contents with the following configuration:
# MySQL Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/medisynk
spring.datasource.username=root
spring.datasource.password=your-password-here
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Seed data
spring.jpa.defer-datasource-initialization=true
spring.sql.init.mode=always
# Spring AI - Google Gemini
spring.ai.google.genai.api-key=${GEMINI_API_KEY}
What do these properties do?
The spring.datasource lines tell Spring Boot the URL, username, and password for your MySQL database.
spring.jpa.hibernate.ddl-auto=update tells Hibernate to automatically create and update database tables based on your Java classes. spring.jpa.show-sql=true logs every SQL query so you can see what's happening under the hood.
spring.jpa.defer-datasource-initialization=true ensures that seed data scripts run after Hibernate creates the tables. Without it, the seed script would run before the tables exist.
The ${GEMINI_API_KEY} syntax reads your API key from an environment variable, keeping the secret out of your source code.
- Replace your-password-here with your actual MySQL root password.
- Save application.properties.
The last property references an environment variable called GEMINI_API_KEY. You need to set this in your terminal before running the app.
- Set your Gemini API key as an environment variable by running this command in the VS Code terminal:
macOS
export GEMINI_API_KEY="your-gemini-api-key-here"
Windows
$env:GEMINI_API_KEY="your-gemini-api-key-here"
- Replace your-gemini-api-key-here with the actual API key from your Google AI Studio account.
Environment variables reset each session
This environment variable only lasts for the current terminal session. If you close the terminal or restart VS Code, you'll need to set it again before running the app.
Verify the project runs
With the database created and configuration in place, the app should start up and connect to MySQL. This confirms everything is wired correctly before you start writing code.
- Make sure you are in the medisynk project folder in your terminal. Start the application by running:
macOS
./mvnw spring-boot:run
Windows
mvnw.cmd spring-boot:run
Maven will download dependencies on the first run, which may take a minute or two. Once complete, you should see a line that says Started MedisynkApplication in the terminal output.
✔️ I see Started MedisynkApplication
Your project is configured correctly and connected to MySQL.
- Stop the application by pressing Ctrl+C in the terminal.
ⓧ I see an error
Check these common causes:
- Access denied for user 'root' means the password in application.properties does not match your MySQL root password. Open the file and correct it.
- Communications link failure means MySQL is not running. Start the MySQL service and try again.
- Unknown database 'medisynk' means the database was not created. Go back to the MySQL step and run CREATE DATABASE medisynk; again.
- If you see a different error, make sure you set the GEMINI_API_KEY environment variable in the same terminal session where you are running the app.
Help me fix my Spring Boot startup error
Your Spring Boot project is wired up with MySQL and Spring AI. Next up, you'll build the data layer with JPA entities that represent medications and prescriptions.
Build the Data Layer
Your Spring Boot project is running and connected to MySQL. Now you need to give it something to store. Before MediSynk can process prescriptions, it needs a database structure that represents the pharmacy's world: medications, prescriptions, and the line items connecting them.
In this step, you will create JPA entities that map Java classes to MySQL tables. Spring Data JPA will auto-generate the tables and give you ready-made query methods with zero SQL.
In this step, get ready to:
- Create the Medication entity and its repository interface.
- Create the Prescription and PrescriptionItem entities with JPA relationships.
- Seed the database with 10 real medications for testing.
Create the Medication entity and repository
A JPA entity is a plain Java class that maps directly to a database table. Each field becomes a column, and each instance becomes a row.
- In VS Code's Explorer sidebar, navigate to src/main/java/com/medisynk.
- Create a new folder called model inside medisynk.
- Inside the model folder, create a new file called Medication.java and paste the following code:
package com.medisynk.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
// Maps this class to the 'medications' table in MySQL
@Entity
@Table(name = "medications")
public class Medication {
// Auto-generated primary key using MySQL's auto-increment
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Each medication name must be unique in the database
@Column(nullable = false, unique = true)
private String name;
@Column(nullable = false)
private String category;
private String dosageForm;
@Column(nullable = false)
private Integer stockQuantity;
private Double price;
// Allow longer text for medication descriptions
@Column(length = 500)
private String description;
// JPA requires a no-argument constructor
public Medication() {
}
// Getters and setters for all fields
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getDosageForm() {
return dosageForm;
}
public void setDosageForm(String dosageForm) {
this.dosageForm = dosageForm;
}
public Integer getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(Integer stockQuantity) {
this.stockQuantity = stockQuantity;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
What does this code do?
@Entity tells JPA that this class represents a database table. @Table(name = "medications") sets the exact table name in MySQL.
The @Column annotations control how each field maps to a column. nullable = false means the column cannot be empty. unique = true on name prevents duplicate medication names.
@GeneratedValue(strategy = GenerationType.IDENTITY) lets MySQL auto-increment the id column for each new row.
- Save Medication.java.
- Check that VS Code shows no red error underlines in the file. The imports should all resolve to jakarta.persistence packages.
Seeing red underlines on the imports?
- Make sure the package declaration at the top says package com.medisynk.model; and that the file is inside the model folder.
- If you see javax.persistence instead of jakarta.persistence, make sure you copied the imports exactly. Spring Boot 3.x uses the jakarta namespace.
- Try running mvnw.cmd clean compile in the terminal to force Maven to resolve the dependencies.
Help me fix import errors in my Medication.java entity.
The entity defines the table structure. Now you need a repository interface so Spring can query it.
- In the Explorer sidebar, create a new folder called repository inside src/main/java/com/medisynk.
- Inside the repository folder, create a new file called MedicationRepository.java and paste the following code:
package com.medisynk.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medisynk.model.Medication;
// Spring Data JPA auto-implements this interface at runtime
public interface MedicationRepository extends JpaRepository<Medication, Long> {
// Spring generates a SQL query from the method name automatically
List<Medication> findByNameContainingIgnoreCase(String name);
}
What does this code do?
By extending JpaRepository<Medication, Long>, this interface automatically gets methods like findAll(), findById(), save(), and delete(). You never write the implementation.
The custom method findByNameContainingIgnoreCase is called a derived query method. Spring Data JPA reads the method name and auto-generates a SQL LIKE query at runtime.
- Save MedicationRepository.java. You won't see a visible change yet. The next substep adds the remaining entities that use this repository.
Create the Prescription entities and status enum
A prescription in MediSynk connects a patient to one or more medications. This requires three pieces: a status enum tracking the prescription's lifecycle, a Prescription entity, and a PrescriptionItem entity that links prescriptions to medications.
- Inside the model folder, create a new file called PrescriptionStatus.java and paste the following code:
package com.medisynk.model;
// Tracks the prescription's lifecycle through AI analysis
public enum PrescriptionStatus {
PENDING,
APPROVED,
FLAGGED,
REJECTED
}
What do these statuses mean?
- PENDING means the prescription has been submitted but not yet analyzed by the AI.
- APPROVED means the AI found no dangerous interactions.
- FLAGGED means the AI found a warning-level interaction that needs pharmacist review.
- REJECTED means the AI found a critical interaction that should not be dispensed.
- Save PrescriptionStatus.java.
Now create the main Prescription entity. This is the largest model because it holds the AI analysis results and connects to prescription items.
- Inside the model folder, create a new file called Prescription.java and paste the following code:
package com.medisynk.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "prescriptions")
public class Prescription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String patientName;
// Store the enum value as a readable string in the database
@Enumerated(EnumType.STRING)
private PrescriptionStatus status;
// Hold the AI's analysis text (allow up to 2000 characters)
@Column(length = 2000)
private String aiAnalysis;
private String severityLevel;
// One prescription has many line items, loaded eagerly for JSON serialization
@JsonManagedReference
@OneToMany(mappedBy = "prescription", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<PrescriptionItem> items = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime processedAt;
public Prescription() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPatientName() {
return patientName;
}
public void setPatientName(String patientName) {
this.patientName = patientName;
}
public PrescriptionStatus getStatus() {
return status;
}
public void setStatus(PrescriptionStatus status) {
this.status = status;
}
public String getAiAnalysis() {
return aiAnalysis;
}
public void setAiAnalysis(String aiAnalysis) {
this.aiAnalysis = aiAnalysis;
}
public String getSeverityLevel() {
return severityLevel;
}
public void setSeverityLevel(String severityLevel) {
this.severityLevel = severityLevel;
}
public List<PrescriptionItem> getItems() {
return items;
}
public void setItems(List<PrescriptionItem> items) {
this.items = items;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getProcessedAt() {
return processedAt;
}
public void setProcessedAt(LocalDateTime processedAt) {
this.processedAt = processedAt;
}
}
What's happening here?
- @Enumerated(EnumType.STRING) stores the status as readable text (PENDING, APPROVED) in the database instead of integer codes.
- @OneToMany creates a one-to-many relationship. One prescription can have multiple medication items. cascade = CascadeType.ALL means saving a prescription automatically saves its items too.
- @JsonManagedReference tells Jackson to serialize the items list when converting a Prescription to JSON. Its partner annotation (@JsonBackReference) goes on the other side to prevent infinite loops.
- Save Prescription.java. You may see a red underline on PrescriptionItem because that class does not exist yet.
Now create the PrescriptionItem entity. This is the join entity that links a prescription to a specific medication with dosage and quantity details.
- Inside the model folder, create a new file called PrescriptionItem.java and paste the following code:
package com.medisynk.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "prescription_items")
public class PrescriptionItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Prevents circular JSON: this side is NOT serialized
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "prescription_id")
private Prescription prescription;
// Load the medication details eagerly so they appear in JSON responses
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "medication_id")
private Medication medication;
private String dosage;
private Integer quantity;
public PrescriptionItem() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Prescription getPrescription() {
return prescription;
}
public void setPrescription(Prescription prescription) {
this.prescription = prescription;
}
public Medication getMedication() {
return medication;
}
public void setMedication(Medication medication) {
this.medication = medication;
}
public String getDosage() {
return dosage;
}
public void setDosage(String dosage) {
this.dosage = dosage;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
}
What does this code do?
- @ManyToOne on prescription creates the "many items belong to one prescription" relationship. @JoinColumn sets the foreign key column name in the database.
- @JsonBackReference on the prescription field prevents infinite JSON loops. When a Prescription is serialized, its items list is included. But each item does NOT include the parent Prescription again.
- FetchType.EAGER on the medication field means the full medication details load immediately when a prescription item is fetched. This ensures the medication name and details appear in API responses.
- Save PrescriptionItem.java. The red underline in Prescription.java should now disappear.
Still seeing errors in Prescription.java or PrescriptionItem.java?
- Confirm all four model files (Medication.java, PrescriptionStatus.java, Prescription.java, PrescriptionItem.java) are inside the same com.medisynk.model package.
- Check that the JsonManagedReference and JsonBackReference imports come from com.fasterxml.jackson.annotation. Jackson is included automatically by the Spring Web starter.
Help me fix errors in my JPA entity files.
The last piece is a repository for prescriptions. This follows the same pattern as the medication repository.
- Inside the repository folder, create a new file called PrescriptionRepository.java and paste the following code:
package com.medisynk.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medisynk.model.Prescription;
import com.medisynk.model.PrescriptionStatus;
public interface PrescriptionRepository extends JpaRepository<Prescription, Long> {
// Find all prescriptions with a specific status (e.g. FLAGGED for pharmacist review)
List<Prescription> findByStatus(PrescriptionStatus status);
}
What does this code do?
The findByStatus method is another derived query. Spring Data JPA reads the method name and generates a WHERE status = ? query at runtime. Later, you will use this to build a pharmacist dashboard that shows only flagged prescriptions.
- Save PrescriptionRepository.java.
Seed the database with medication data
Your entities define the table structure, but the tables are empty. MediSynk needs real medications with known drug interactions so you can test the AI analyzer later.
- Inside the src/main/resources folder (the same folder that contains application.properties), create a new file called data.sql and paste the following code:
INSERT IGNORE INTO medications (id, name, category, dosage_form, stock_quantity, price, description) VALUES
(1, 'Amoxicillin', 'Antibiotic', 'Capsule', 500, 8.99, 'Broad-spectrum penicillin antibiotic'),
(2, 'Ibuprofen', 'NSAID', 'Tablet', 1000, 5.49, 'Anti-inflammatory pain reliever'),
(3, 'Lisinopril', 'ACE Inhibitor', 'Tablet', 300, 12.99, 'Blood pressure medication'),
(4, 'Metformin', 'Antidiabetic', 'Tablet', 400, 7.49, 'Type 2 diabetes medication'),
(5, 'Warfarin', 'Anticoagulant', 'Tablet', 200, 15.99, 'Blood thinner for clot prevention'),
(6, 'Aspirin', 'NSAID', 'Tablet', 800, 3.99, 'Pain reliever and blood thinner'),
(7, 'Omeprazole', 'Proton Pump Inhibitor', 'Capsule', 350, 9.49, 'Acid reflux and ulcer medication'),
(8, 'Simvastatin', 'Statin', 'Tablet', 250, 11.99, 'Cholesterol-lowering medication'),
(9, 'Ciprofloxacin', 'Antibiotic', 'Tablet', 150, 14.49, 'Fluoroquinolone antibiotic'),
(10, 'Prednisone', 'Corticosteroid', 'Tablet', 200, 6.99, 'Anti-inflammatory steroid');
What does this code do?
INSERT IGNORE INTO is MySQL-specific syntax. It inserts the row if it does not already exist, and silently skips it if it does. This prevents duplicate-key errors when you restart the application.
These 10 medications include pairs with known drug interactions. For example, Warfarin combined with Aspirin or Ibuprofen creates serious bleeding risks. You will use these combinations to test the AI analyzer in a later step.
Why does data.sql run after the tables are created?
In Step 1, you set spring.jpa.defer-datasource-initialization=true in application.properties. This tells Spring Boot to wait for Hibernate to create the tables from your entities before running data.sql. Without this setting, data.sql would execute before the tables exist and fail.
- Save data.sql.
- Make sure your GEMINI_API_KEY environment variable is still set in your PowerShell terminal from Step 1.
- Start the application by running:
mvnw.cmd spring-boot:run
- Watch the console output. You should see Hibernate SQL statements creating the medications, prescriptions, and prescription_items tables, followed by Started MedisynkApplication.
- Stop the application with Ctrl+C.
Now confirm that the seed data landed in MySQL.
- Open a new terminal and log into MySQL by running:
mysql -u root -p
- Query the medications table by running:
SELECT * FROM medisynk.medications;
- You should see 10 rows in the result, one for each medication from Amoxicillin to Prednisone.
- Type exit to leave the MySQL prompt.
Application won't start or no rows in the table?
- If you see a Table 'medisynk.medications' doesn't exist error, check that your Medication.java has @Entity and @Table(name = "medications") annotations. Hibernate uses these to create the table.
- If the app starts but the table has 0 rows, check that data.sql is in src/main/resources (not inside a subfolder). Also confirm spring.sql.init.mode=always is in your application.properties.
- If you see a column name mismatch error, check that your data.sql uses dosage_form and stock_quantity (snake_case). Hibernate converts Java's camelCase field names to snake_case column names by default.
Help me fix my data.sql seed data or Hibernate table creation.
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files before moving on.
ⓧ I'd like to double check the full code
Here is every file you created or modified in this step. Compare them against your code.
package com.medisynk.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "medications")
public class Medication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@Column(nullable = false)
private String category;
private String dosageForm;
@Column(nullable = false)
private Integer stockQuantity;
private Double price;
@Column(length = 500)
private String description;
public Medication() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getDosageForm() {
return dosageForm;
}
public void setDosageForm(String dosageForm) {
this.dosageForm = dosageForm;
}
public Integer getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(Integer stockQuantity) {
this.stockQuantity = stockQuantity;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
package com.medisynk.model;
public enum PrescriptionStatus {
PENDING,
APPROVED,
FLAGGED,
REJECTED
}
package com.medisynk.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
@Entity
@Table(name = "prescriptions")
public class Prescription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String patientName;
@Enumerated(EnumType.STRING)
private PrescriptionStatus status;
@Column(length = 2000)
private String aiAnalysis;
private String severityLevel;
@JsonManagedReference
@OneToMany(mappedBy = "prescription", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<PrescriptionItem> items = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime processedAt;
public Prescription() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPatientName() {
return patientName;
}
public void setPatientName(String patientName) {
this.patientName = patientName;
}
public PrescriptionStatus getStatus() {
return status;
}
public void setStatus(PrescriptionStatus status) {
this.status = status;
}
public String getAiAnalysis() {
return aiAnalysis;
}
public void setAiAnalysis(String aiAnalysis) {
this.aiAnalysis = aiAnalysis;
}
public String getSeverityLevel() {
return severityLevel;
}
public void setSeverityLevel(String severityLevel) {
this.severityLevel = severityLevel;
}
public List<PrescriptionItem> getItems() {
return items;
}
public void setItems(List<PrescriptionItem> items) {
this.items = items;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getProcessedAt() {
return processedAt;
}
public void setProcessedAt(LocalDateTime processedAt) {
this.processedAt = processedAt;
}
}
package com.medisynk.model;
import com.fasterxml.jackson.annotation.JsonBackReference;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "prescription_items")
public class PrescriptionItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "prescription_id")
private Prescription prescription;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "medication_id")
private Medication medication;
private String dosage;
private Integer quantity;
public PrescriptionItem() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Prescription getPrescription() {
return prescription;
}
public void setPrescription(Prescription prescription) {
this.prescription = prescription;
}
public Medication getMedication() {
return medication;
}
public void setMedication(Medication medication) {
this.medication = medication;
}
public String getDosage() {
return dosage;
}
public void setDosage(String dosage) {
this.dosage = dosage;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
}
package com.medisynk.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medisynk.model.Medication;
public interface MedicationRepository extends JpaRepository<Medication, Long> {
List<Medication> findByNameContainingIgnoreCase(String name);
}
package com.medisynk.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medisynk.model.Prescription;
import com.medisynk.model.PrescriptionStatus;
public interface PrescriptionRepository extends JpaRepository<Prescription, Long> {
List<Prescription> findByStatus(PrescriptionStatus status);
}
INSERT IGNORE INTO medications (id, name, category, dosage_form, stock_quantity, price, description) VALUES
(1, 'Amoxicillin', 'Antibiotic', 'Capsule', 500, 8.99, 'Broad-spectrum penicillin antibiotic'),
(2, 'Ibuprofen', 'NSAID', 'Tablet', 1000, 5.49, 'Anti-inflammatory pain reliever'),
(3, 'Lisinopril', 'ACE Inhibitor', 'Tablet', 300, 12.99, 'Blood pressure medication'),
(4, 'Metformin', 'Antidiabetic', 'Tablet', 400, 7.49, 'Type 2 diabetes medication'),
(5, 'Warfarin', 'Anticoagulant', 'Tablet', 200, 15.99, 'Blood thinner for clot prevention'),
(6, 'Aspirin', 'NSAID', 'Tablet', 800, 3.99, 'Pain reliever and blood thinner'),
(7, 'Omeprazole', 'Proton Pump Inhibitor', 'Capsule', 350, 9.49, 'Acid reflux and ulcer medication'),
(8, 'Simvastatin', 'Statin', 'Tablet', 250, 11.99, 'Cholesterol-lowering medication'),
(9, 'Ciprofloxacin', 'Antibiotic', 'Tablet', 150, 14.49, 'Fluoroquinolone antibiotic'),
(10, 'Prednisone', 'Corticosteroid', 'Tablet', 200, 6.99, 'Anti-inflammatory steroid');
Your data layer is complete. MediSynk now has three connected database tables and 10 real medications ready for testing. Next up, you will expose this data through REST API endpoints so you can interact with it from outside the application.
Create the Medication REST API
Your data layer is locked in. You have entities, repositories, and 10 real medications seeded in MySQL. The database knows how to store and retrieve medications, but there is no way for the outside world to access them yet.
In this step, you will build the REST API layer that exposes your medication data over HTTP. This means creating a Service class to hold the business logic and a Controller class to handle incoming web requests.
In this step, get ready to:
- Create the MedicationService class with business logic methods.
- Create the MedicationController with four REST endpoints.
- Test the API by querying your seeded medications with curl.
Create the MedicationService class
In Spring Boot's layered architecture, the Service class sits between the Controller and the Repository. The Controller handles HTTP requests, the Service holds the business logic, and the Repository talks to the database.
Why separate Service from Controller?
You could put database calls directly in the Controller, but that tangles your HTTP logic with your business rules. A separate Service class keeps each layer focused on one job.
When you add the AI prescription analyzer in the next step, the Service layer is where that logic will live. The Controller stays thin and only routes requests.
- In VS Code's Explorer sidebar, right-click the com.medisynk package inside src/main/java.
- Select New Folder and name it service.
- Inside the service folder, create a new file called MedicationService.java and paste the following code:
package com.medisynk.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medisynk.model.Medication;
import com.medisynk.repository.MedicationRepository;
// Marks this class as a Spring-managed service component
@Service
public class MedicationService {
// Repository dependency injected through the constructor
private final MedicationRepository medicationRepository;
public MedicationService(MedicationRepository medicationRepository) {
this.medicationRepository = medicationRepository;
}
// Returns every medication in the database
public List<Medication> getAllMedications() {
return medicationRepository.findAll();
}
// Finds a single medication or throws if the ID doesn't exist
public Medication getMedicationById(Long id) {
return medicationRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Medication not found with id: " + id));
}
// Searches by partial name, case-insensitive
public List<Medication> searchMedications(String name) {
return medicationRepository.findByNameContainingIgnoreCase(name);
}
// Persists a new medication to the database
public Medication addMedication(Medication medication) {
return medicationRepository.save(medication);
}
}
What does this code do?
- @Service tells Spring to manage this class as a bean. Spring creates one instance and injects it wherever it is needed.
- The constructor takes a MedicationRepository parameter. Spring automatically passes in the repository you created in the previous step. This pattern is called constructor injection.
- getMedicationById() uses orElseThrow() to throw an error if no medication matches the given ID, instead of returning null.
- searchMedications() delegates to the custom query method you defined on the repository. Spring Data JPA implements the actual SQL behind the scenes.
- Save MedicationService.java.
You won't see a change yet. The service needs a controller in front of it before you can call it over HTTP.
✔️ Awesome, I've got everything!
Double check you have saved MedicationService.java.
ⓧ I'd like to double check the full code
package com.medisynk.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medisynk.model.Medication;
import com.medisynk.repository.MedicationRepository;
@Service
public class MedicationService {
private final MedicationRepository medicationRepository;
public MedicationService(MedicationRepository medicationRepository) {
this.medicationRepository = medicationRepository;
}
public List<Medication> getAllMedications() {
return medicationRepository.findAll();
}
public Medication getMedicationById(Long id) {
return medicationRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Medication not found with id: " + id));
}
public List<Medication> searchMedications(String name) {
return medicationRepository.findByNameContainingIgnoreCase(name);
}
public Medication addMedication(Medication medication) {
return medicationRepository.save(medication);
}
}
Create the MedicationController
The Controller is the front door of your API. It maps HTTP requests to the service methods you just wrote and wraps the results in proper HTTP responses.
- In VS Code's Explorer sidebar, right-click the com.medisynk package inside src/main/java.
- Select New Folder and name it controller.
- Inside the controller folder, create a new file called MedicationController.java and paste the following code:
package com.medisynk.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.medisynk.model.Medication;
import com.medisynk.service.MedicationService;
// REST controller that handles all /api/medications requests
@RestController
@RequestMapping("/api/medications")
public class MedicationController {
private final MedicationService medicationService;
// Spring injects MedicationService automatically
public MedicationController(MedicationService medicationService) {
this.medicationService = medicationService;
}
// GET /api/medications - returns all medications
@GetMapping
public ResponseEntity<List<Medication>> getAllMedications() {
return ResponseEntity.ok(medicationService.getAllMedications());
}
// GET /api/medications/{id} - returns one medication by ID
@GetMapping("/{id}")
public ResponseEntity<Medication> getMedicationById(@PathVariable Long id) {
return ResponseEntity.ok(medicationService.getMedicationById(id));
}
// GET /api/medications/search?name=... - searches by partial name
@GetMapping("/search")
public ResponseEntity<List<Medication>> searchMedications(@RequestParam String name) {
return ResponseEntity.ok(medicationService.searchMedications(name));
}
// POST /api/medications - creates a new medication, returns 201
@PostMapping
public ResponseEntity<Medication> addMedication(@RequestBody Medication medication) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(medicationService.addMedication(medication));
}
}
What does this code do?
- @RestController tells Spring this class handles HTTP requests and automatically converts return values to JSON.
- @RequestMapping("/api/medications") sets the base URL path. Every endpoint method in this class builds on top of /api/medications.
- Each method returns a ResponseEntity, which wraps the response body together with an HTTP status code. The addMedication method returns 201 CREATED instead of the default 200 OK.
- @PathVariable pulls values from the URL path (like /api/medications/5). @RequestParam pulls values from query parameters (like ?name=war).
- Save MedicationController.java.
Both new files are saved. You will verify everything works in the next substep when you start the application and hit the endpoints.
✔️ Awesome, I've got everything!
Double check you have saved MedicationController.java.
ⓧ I'd like to double check the full code
package com.medisynk.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.medisynk.model.Medication;
import com.medisynk.service.MedicationService;
@RestController
@RequestMapping("/api/medications")
public class MedicationController {
private final MedicationService medicationService;
public MedicationController(MedicationService medicationService) {
this.medicationService = medicationService;
}
@GetMapping
public ResponseEntity<List<Medication>> getAllMedications() {
return ResponseEntity.ok(medicationService.getAllMedications());
}
@GetMapping("/{id}")
public ResponseEntity<Medication> getMedicationById(@PathVariable Long id) {
return ResponseEntity.ok(medicationService.getMedicationById(id));
}
@GetMapping("/search")
public ResponseEntity<List<Medication>> searchMedications(@RequestParam String name) {
return ResponseEntity.ok(medicationService.searchMedications(name));
}
@PostMapping
public ResponseEntity<Medication> addMedication(@RequestBody Medication medication) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(medicationService.addMedication(medication));
}
}
Test the medication endpoints
Your service and controller are wired up. Time to start the application and confirm the endpoints return real data from MySQL.
Set your API key first
If you closed your terminal since Step 1, you need to set the Gemini API key environment variable again. The app will fail to start without it.
In PowerShell, run: $env:GEMINI_API_KEY="your-gemini-api-key-here" before starting the application.
- Start the application by running this command from your project's root folder:
mvnw.cmd spring-boot:run
- Wait until you see Started MedisynkApplication in the terminal output.
- Open a second terminal window. You need the first terminal to keep the app running.
Application won't start?
- If you see a compilation error, check that both new files are inside the correct packages: com.medisynk.service and com.medisynk.controller. The folder structure must match.
- If you see Port 8080 already in use, stop any previous instance of the app with Ctrl+C before restarting.
- Help me debug my Spring Boot startup error.
- Fetch all medications by running this command in your second terminal:
curl http://localhost:8080/api/medications
You should see a JSON array containing all 10 seeded medications, starting with Amoxicillin and ending with Prednisone.
- Test the search endpoint by running:
curl http://localhost:8080/api/medications/search?name=war
This should return a JSON array with one result: Warfarin. The search is case-insensitive, so war matches Warfarin.
Getting an empty array or an error?
- An empty array from /api/medications means the seed data did not load. Check that data.sql is in src/main/resources/ and that spring.sql.init.mode=always is set in application.properties.
- A 404 Not Found error means the controller is not being picked up. Verify the @RestController and @RequestMapping annotations are present on your controller class.
- Help me figure out why my medication endpoint is not returning data.
Your medication API is live. You can list, search, and add medications over HTTP. Next up, you will build the AI-powered prescription analyzer that uses Google Gemini to check drug interactions automatically.
Build the AI Prescription Analyzer
Your medication REST API is live and returning data from MySQL. Now it is time to add the intelligence that makes MediSynk more than a basic CRUD app.
In this step, you will connect your application to Google Gemini using Spring AI. When a prescription comes in, the system will look up each medication, send the combination to Gemini for drug interaction analysis, and automatically decide whether to approve, flag, or reject it.
In this step, get ready to:
- Create the AI interaction service that sends medication data to Gemini and receives structured analysis.
- Build the prescription processing workflow that auto-approves safe prescriptions and flags dangerous ones.
- Create REST endpoints for submitting and reviewing prescriptions.
Create the AI interaction service
Spring AI's ChatClient can return AI responses as structured Java objects instead of plain text. You define a Java record describing the shape of the response, and Spring AI instructs the model to return JSON matching that shape.
You will start by creating the DTO (Data Transfer Object) that defines what the AI's interaction report looks like. Then you will build the service that talks to Gemini.
- Create a new file at src/main/java/com/medisynk/dto/InteractionReport.java and add the following code:
package com.medisynk.dto;
import java.util.List;
// Structured output that the AI must return
// Spring AI converts the model's JSON response into this record
public record InteractionReport(
String severity,
boolean autoApproved,
List<String> interactions,
String recommendation
) {
}
What does this code do?
A Java record is a compact way to define a class that just holds data. This record has four fields:
- severity tells you how dangerous the drug combination is (SAFE, WARNING, or CRITICAL).
- autoApproved is a boolean flag the AI sets to true only when the prescription is completely safe.
- interactions lists each specific drug interaction the AI found.
- recommendation provides a brief message for the pharmacist.
- Save InteractionReport.java.
- You will not see a change yet. The next chunk creates the AI service that returns this record.
- Create a new file at src/main/java/com/medisynk/service/AiInteractionService.java and add the following code:
package com.medisynk.service;
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import com.medisynk.dto.InteractionReport;
@Service
public class AiInteractionService {
private final ChatClient chatClient;
// Build a ChatClient with a system prompt that sets the AI's role
public AiInteractionService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("""
You are a pharmaceutical AI assistant specialized in drug interaction analysis.
Analyze medication combinations for potential drug-drug interactions,
contraindications, and dosage concerns.
Be thorough but concise. Always err on the side of caution for patient safety.
""")
.build();
}
// Send medication details to Gemini and get a structured InteractionReport back
public InteractionReport analyzePrescription(List<String> medicationDetails) {
String medicationList = String.join(", ", medicationDetails);
String prompt = String.format("""
Analyze the following prescription for drug interactions and safety:
Medications: %s
Evaluate:
1. Drug-drug interactions between these specific medications
2. Dosage appropriateness
3. Common contraindications
Determine severity as exactly one of: SAFE, WARNING, CRITICAL
Set autoApproved to true ONLY if severity is SAFE and there are no concerns.
List each specific interaction found.
Provide a brief recommendation for the pharmacist.
""", medicationList);
// .entity(InteractionReport.class) tells Spring AI to parse the response as JSON
return chatClient.prompt()
.user(prompt)
.call()
.entity(InteractionReport.class);
}
}
What does this code do?
This service is the bridge between your Spring Boot application and Google Gemini.
- The constructor injects ChatClient.Builder, which Spring AI auto-configures when the Google GenAI starter is on your classpath. The defaultSystem() call sets a system prompt that tells Gemini to act as a pharmaceutical AI assistant.
- The analyzePrescription() method joins the medication names into a prompt, sends it to Gemini, and calls .entity(InteractionReport.class) to automatically parse the AI's JSON response into your Java record.
How does structured output work?
When you call .entity(InteractionReport.class), Spring AI uses a BeanOutputConverter behind the scenes. It appends format instructions to your prompt telling the model to return JSON matching the Java record's field structure. Then it deserializes the response back into an InteractionReport object.
- Save AiInteractionService.java.
- Verify the project still compiles by running this command in your terminal:
mvnw.cmd spring-boot:run
You should see Started MedisynkApplication in the terminal output. This confirms Spring found your new service and wired it up correctly.
- Stop the application with Ctrl+C.
Application fails to start?
- If you see package com.medisynk.dto does not exist, check that InteractionReport.java is inside the src/main/java/com/medisynk/dto/ folder. Create the dto folder if it does not exist.
- If you see an error about ChatClient not being found, confirm your pom.xml has the spring-ai-starter-model-google-genai dependency and the Spring AI BOM.
- Make sure the GEMINI_API_KEY environment variable is still set in your terminal session. If you opened a new terminal, you need to set it again.
Still stuck? Help me debug why my Spring Boot app fails to start after adding AiInteractionService.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full code for both files you created in this substep.
package com.medisynk.dto;
import java.util.List;
public record InteractionReport(
String severity,
boolean autoApproved,
List<String> interactions,
String recommendation
) {
}
package com.medisynk.service;
import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import com.medisynk.dto.InteractionReport;
@Service
public class AiInteractionService {
private final ChatClient chatClient;
public AiInteractionService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("""
You are a pharmaceutical AI assistant specialized in drug interaction analysis.
Analyze medication combinations for potential drug-drug interactions,
contraindications, and dosage concerns.
Be thorough but concise. Always err on the side of caution for patient safety.
""")
.build();
}
public InteractionReport analyzePrescription(List<String> medicationDetails) {
String medicationList = String.join(", ", medicationDetails);
String prompt = String.format("""
Analyze the following prescription for drug interactions and safety:
Medications: %s
Evaluate:
1. Drug-drug interactions between these specific medications
2. Dosage appropriateness
3. Common contraindications
Determine severity as exactly one of: SAFE, WARNING, CRITICAL
Set autoApproved to true ONLY if severity is SAFE and there are no concerns.
List each specific interaction found.
Provide a brief recommendation for the pharmacist.
""", medicationList);
return chatClient.prompt()
.user(prompt)
.call()
.entity(InteractionReport.class);
}
}
Create the prescription processing service
The AI service can now analyze medications, but it needs a workflow to drive it. The prescription service is the core engine that receives a prescription request, looks up each medication in the database, sends the combination to the AI, and updates the prescription status based on the result.
You will start with a request DTO that defines what the incoming prescription JSON looks like. Then you will build the service that orchestrates the entire flow.
- Create a new file at src/main/java/com/medisynk/dto/PrescriptionRequest.java and add the following code:
package com.medisynk.dto;
import java.util.List;
// Incoming JSON shape for a new prescription submission
public record PrescriptionRequest(
String patientName,
List<PrescriptionItemRequest> items
) {
// Each item in the prescription references a medication by its database ID
public record PrescriptionItemRequest(
Long medicationId,
String dosage,
Integer quantity
) {
}
}
What does this code do?
This record defines the shape of the JSON body that a client sends when submitting a prescription. It contains a patient name and a list of items. Each item references a medication by its database ID, along with dosage and quantity.
The nested PrescriptionItemRequest record keeps the request and item definitions grouped in one file. Spring Boot automatically deserializes the incoming JSON into these records.
- Save PrescriptionRequest.java.
- You will not see a change yet. The next chunk creates the service that uses this request DTO.
- Create a new file at src/main/java/com/medisynk/service/PrescriptionService.java and add the following code:
package com.medisynk.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.medisynk.dto.InteractionReport;
import com.medisynk.dto.PrescriptionRequest;
import com.medisynk.model.Medication;
import com.medisynk.model.Prescription;
import com.medisynk.model.PrescriptionItem;
import com.medisynk.model.PrescriptionStatus;
import com.medisynk.repository.MedicationRepository;
import com.medisynk.repository.PrescriptionRepository;
@Service
public class PrescriptionService {
private final PrescriptionRepository prescriptionRepository;
private final MedicationRepository medicationRepository;
private final AiInteractionService aiInteractionService;
public PrescriptionService(PrescriptionRepository prescriptionRepository,
MedicationRepository medicationRepository,
AiInteractionService aiInteractionService) {
this.prescriptionRepository = prescriptionRepository;
this.medicationRepository = medicationRepository;
this.aiInteractionService = aiInteractionService;
}
// Core workflow: receive request -> look up meds -> call AI -> set status -> save
@Transactional
public Prescription processPrescription(PrescriptionRequest request) {
Prescription prescription = new Prescription();
prescription.setPatientName(request.patientName());
prescription.setStatus(PrescriptionStatus.PENDING);
prescription.setCreatedAt(LocalDateTime.now());
List<String> medicationDetails = new ArrayList<>();
// Look up each medication and build the prescription items
for (PrescriptionRequest.PrescriptionItemRequest itemRequest : request.items()) {
Medication medication = medicationRepository.findById(itemRequest.medicationId())
.orElseThrow(() -> new RuntimeException(
"Medication not found with id: " + itemRequest.medicationId()));
PrescriptionItem item = new PrescriptionItem();
item.setPrescription(prescription);
item.setMedication(medication);
item.setDosage(itemRequest.dosage());
item.setQuantity(itemRequest.quantity());
prescription.getItems().add(item);
medicationDetails.add(medication.getName() + " (" + itemRequest.dosage() + ")");
}
// Send medication list to the AI for interaction analysis
InteractionReport report = aiInteractionService.analyzePrescription(medicationDetails);
prescription.setAiAnalysis(report.recommendation());
prescription.setSeverityLevel(report.severity());
prescription.setProcessedAt(LocalDateTime.now());
// Decide the prescription status based on the AI report
if (report.autoApproved()) {
prescription.setStatus(PrescriptionStatus.APPROVED);
} else if ("CRITICAL".equals(report.severity())) {
prescription.setStatus(PrescriptionStatus.REJECTED);
} else {
prescription.setStatus(PrescriptionStatus.FLAGGED);
}
return prescriptionRepository.save(prescription);
}
public List<Prescription> getAllPrescriptions() {
return prescriptionRepository.findAll();
}
public Prescription getPrescriptionById(Long id) {
return prescriptionRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Prescription not found with id: " + id));
}
// Returns only prescriptions that need pharmacist review
public List<Prescription> getFlaggedPrescriptions() {
return prescriptionRepository.findByStatus(PrescriptionStatus.FLAGGED);
}
}
What does this code do?
This is the core workflow engine. The processPrescription() method orchestrates the entire prescription processing flow:
- It creates a new Prescription entity with PENDING status and a timestamp.
- It loops through each item in the request, looks up the medication from the database, and creates PrescriptionItem entities linking the medication to the prescription.
- It sends the medication list to AiInteractionService and receives a structured InteractionReport.
- Based on the report, it sets the status to APPROVED (safe), REJECTED (critical), or FLAGGED (needs pharmacist review).
The @Transactional annotation ensures that if anything fails mid-process (bad medication ID, AI error), the entire database operation rolls back cleanly.
- Save PrescriptionService.java.
- You will not see a change yet. The next substep creates the REST controller that exposes these methods as endpoints.
Seeing a red underline on an import?
- If your editor flags com.medisynk.dto.InteractionReport or com.medisynk.dto.PrescriptionRequest as missing, confirm both DTO files are saved inside the dto folder.
- Check that every package declaration matches the file's folder path. For example, PrescriptionService.java should declare package com.medisynk.service and live inside src/main/java/com/medisynk/service/.
Need more help? Help me fix import errors in my PrescriptionService.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full code for both files you created in this substep.
package com.medisynk.dto;
import java.util.List;
public record PrescriptionRequest(
String patientName,
List<PrescriptionItemRequest> items
) {
public record PrescriptionItemRequest(
Long medicationId,
String dosage,
Integer quantity
) {
}
}
package com.medisynk.service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.medisynk.dto.InteractionReport;
import com.medisynk.dto.PrescriptionRequest;
import com.medisynk.model.Medication;
import com.medisynk.model.Prescription;
import com.medisynk.model.PrescriptionItem;
import com.medisynk.model.PrescriptionStatus;
import com.medisynk.repository.MedicationRepository;
import com.medisynk.repository.PrescriptionRepository;
@Service
public class PrescriptionService {
private final PrescriptionRepository prescriptionRepository;
private final MedicationRepository medicationRepository;
private final AiInteractionService aiInteractionService;
public PrescriptionService(PrescriptionRepository prescriptionRepository,
MedicationRepository medicationRepository,
AiInteractionService aiInteractionService) {
this.prescriptionRepository = prescriptionRepository;
this.medicationRepository = medicationRepository;
this.aiInteractionService = aiInteractionService;
}
@Transactional
public Prescription processPrescription(PrescriptionRequest request) {
Prescription prescription = new Prescription();
prescription.setPatientName(request.patientName());
prescription.setStatus(PrescriptionStatus.PENDING);
prescription.setCreatedAt(LocalDateTime.now());
List<String> medicationDetails = new ArrayList<>();
for (PrescriptionRequest.PrescriptionItemRequest itemRequest : request.items()) {
Medication medication = medicationRepository.findById(itemRequest.medicationId())
.orElseThrow(() -> new RuntimeException(
"Medication not found with id: " + itemRequest.medicationId()));
PrescriptionItem item = new PrescriptionItem();
item.setPrescription(prescription);
item.setMedication(medication);
item.setDosage(itemRequest.dosage());
item.setQuantity(itemRequest.quantity());
prescription.getItems().add(item);
medicationDetails.add(medication.getName() + " (" + itemRequest.dosage() + ")");
}
InteractionReport report = aiInteractionService.analyzePrescription(medicationDetails);
prescription.setAiAnalysis(report.recommendation());
prescription.setSeverityLevel(report.severity());
prescription.setProcessedAt(LocalDateTime.now());
if (report.autoApproved()) {
prescription.setStatus(PrescriptionStatus.APPROVED);
} else if ("CRITICAL".equals(report.severity())) {
prescription.setStatus(PrescriptionStatus.REJECTED);
} else {
prescription.setStatus(PrescriptionStatus.FLAGGED);
}
return prescriptionRepository.save(prescription);
}
public List<Prescription> getAllPrescriptions() {
return prescriptionRepository.findAll();
}
public Prescription getPrescriptionById(Long id) {
return prescriptionRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Prescription not found with id: " + id));
}
public List<Prescription> getFlaggedPrescriptions() {
return prescriptionRepository.findByStatus(PrescriptionStatus.FLAGGED);
}
}
Create the prescription controller
The service layer is complete. Now you need a REST controller to expose the prescription workflow as HTTP endpoints that clients can call.
- Create a new file at src/main/java/com/medisynk/controller/PrescriptionController.java and add the following code:
package com.medisynk.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medisynk.dto.PrescriptionRequest;
import com.medisynk.model.Prescription;
import com.medisynk.service.PrescriptionService;
@RestController
@RequestMapping("/api/prescriptions")
public class PrescriptionController {
private final PrescriptionService prescriptionService;
public PrescriptionController(PrescriptionService prescriptionService) {
this.prescriptionService = prescriptionService;
}
// Submit a prescription for AI-powered processing
@PostMapping
public ResponseEntity<Prescription> submitPrescription(@RequestBody PrescriptionRequest request) {
Prescription processed = prescriptionService.processPrescription(request);
return ResponseEntity.status(HttpStatus.CREATED).body(processed);
}
// List all prescriptions
@GetMapping
public ResponseEntity<List<Prescription>> getAllPrescriptions() {
return ResponseEntity.ok(prescriptionService.getAllPrescriptions());
}
// Pharmacist dashboard: only flagged prescriptions
@GetMapping("/flagged")
public ResponseEntity<List<Prescription>> getFlaggedPrescriptions() {
return ResponseEntity.ok(prescriptionService.getFlaggedPrescriptions());
}
// Get a single prescription with full details
@GetMapping("/{id}")
public ResponseEntity<Prescription> getPrescriptionById(@PathVariable Long id) {
return ResponseEntity.ok(prescriptionService.getPrescriptionById(id));
}
}
What does this code do?
This controller maps to /api/prescriptions and exposes four endpoints:
- POST /api/prescriptions accepts a JSON prescription request, triggers the full AI analysis workflow, and returns the processed prescription with a 201 CREATED status.
- GET /api/prescriptions lists all prescriptions in the database.
- GET /api/prescriptions/flagged returns only prescriptions that need pharmacist review.
- GET /api/prescriptions/{id} retrieves a single prescription with its full details.
- Save PrescriptionController.java.
- Restart the application by running:
mvnw.cmd spring-boot:run
You should see Started MedisynkApplication in the terminal. Your application now has both the medication and prescription endpoints ready.
Seeing a compilation error?
- If you see cannot find symbol: class PrescriptionService, make sure PrescriptionService.java is saved inside src/main/java/com/medisynk/service/.
- If you see cannot find symbol: class PrescriptionRequest, confirm PrescriptionRequest.java is saved inside src/main/java/com/medisynk/dto/.
- Double check that every file's package declaration matches the folder it lives in. A mismatch between package com.medisynk.controller and the actual folder path causes this error.
Still stuck? Help me fix my PrescriptionController compilation error.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved and leave the application running for the next step.
ⓧ I'd like to double check the full code
Here is the full code for the prescription controller.
package com.medisynk.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medisynk.dto.PrescriptionRequest;
import com.medisynk.model.Prescription;
import com.medisynk.service.PrescriptionService;
@RestController
@RequestMapping("/api/prescriptions")
public class PrescriptionController {
private final PrescriptionService prescriptionService;
public PrescriptionController(PrescriptionService prescriptionService) {
this.prescriptionService = prescriptionService;
}
@PostMapping
public ResponseEntity<Prescription> submitPrescription(@RequestBody PrescriptionRequest request) {
Prescription processed = prescriptionService.processPrescription(request);
return ResponseEntity.status(HttpStatus.CREATED).body(processed);
}
@GetMapping
public ResponseEntity<List<Prescription>> getAllPrescriptions() {
return ResponseEntity.ok(prescriptionService.getAllPrescriptions());
}
@GetMapping("/flagged")
public ResponseEntity<List<Prescription>> getFlaggedPrescriptions() {
return ResponseEntity.ok(prescriptionService.getFlaggedPrescriptions());
}
@GetMapping("/{id}")
public ResponseEntity<Prescription> getPrescriptionById(@PathVariable Long id) {
return ResponseEntity.ok(prescriptionService.getPrescriptionById(id));
}
}
Your AI-powered prescription processing engine is built. In the next step, you will put it to the test by submitting real prescriptions with known drug interactions and watching the automated workflow in action.
Test the Automated Workflow
In the previous step, you built the complete AI prescription workflow. Your PrescriptionService takes in a prescription request, looks up each medication in the database, sends the combination to Google Gemini for analysis, and automatically sets the status based on the AI's findings.
Now it is time to put the system to the test. You will submit two prescriptions with very different risk profiles and see how the automated workflow handles each one.
In this step, get ready to:
- Submit a safe prescription with non-interacting medications and verify it gets auto-approved.
- Submit a dangerous prescription with known drug interactions and verify it gets flagged or rejected.
- Query the pharmacist dashboard endpoint to see flagged prescriptions.
Submit a safe prescription
The seed data includes medications with well-documented interaction profiles. Amoxicillin (an antibiotic) and Metformin (a diabetes medication) have no clinically significant interaction, so the AI should mark this combination as safe.
Set your API key first
If you opened a new terminal since Step 1, your environment variable is gone. Re-set it before starting the app by running:
$env:GEMINI_API_KEY="your-gemini-api-key-here"
- Start the application by running this command from the project root:
mvnw.cmd spring-boot:run
- Wait until you see Started MedisynkApplication in the terminal output.
- Open a second terminal window so you can run curl commands while the app keeps running.
- Submit a prescription for patient "John Doe" with Amoxicillin and Metformin by running this command:
curl -X POST http://localhost:8080/api/prescriptions -H "Content-Type: application/json" -d "{\"patientName\": \"John Doe\", \"items\": [{\"medicationId\": 1, \"dosage\": \"500mg 3x daily\", \"quantity\": 21}, {\"medicationId\": 4, \"dosage\": \"850mg 2x daily\", \"quantity\": 60}]}"
What does this request do?
This sends a JSON body to the POST /api/prescriptions endpoint. The controller hands the request to PrescriptionService, which looks up Amoxicillin (ID 1) and Metformin (ID 4) from the database.
It then sends the medication names and dosages to Gemini for interaction analysis. Gemini returns a structured InteractionReport, and the service uses the report's severity and autoApproved flag to set the prescription's final status.
The response may take a few seconds while Gemini analyzes the prescription. You should see a JSON response with "status": "APPROVED" and "severityLevel": "SAFE".
The aiAnalysis field contains the pharmacist recommendation from Gemini, confirming there are no significant interactions between these two drugs.
Seeing a connection error or 500 response?
- Make sure the app is still running in your first terminal. You should see Started MedisynkApplication in the output.
- Check that your GEMINI_API_KEY environment variable is set in the same terminal where you started the app.
- If you see a 500 error with a JSON parsing message, Gemini may have returned an unexpected format. Stop the app with Ctrl+C and restart it, then try the curl command again.
Help me debug my prescription submission error.
Submit a dangerous prescription
The safe test passed. Now let's see what happens when the AI encounters a genuinely dangerous drug combination.
Warfarin is a blood thinner. Aspirin also thins the blood. Ibuprofen is an NSAID that increases bleeding risk further. Combining all three creates a serious, well-documented bleeding hazard.
- Submit a prescription for patient "Jane Smith" with Warfarin, Aspirin, and Ibuprofen by running this command:
curl -X POST http://localhost:8080/api/prescriptions -H "Content-Type: application/json" -d "{\"patientName\": \"Jane Smith\", \"items\": [{\"medicationId\": 5, \"dosage\": \"5mg daily\", \"quantity\": 30}, {\"medicationId\": 6, \"dosage\": \"100mg daily\", \"quantity\": 30}, {\"medicationId\": 2, \"dosage\": \"400mg 3x daily\", \"quantity\": 90}]}"
What should I see?
The response should show a status of FLAGGED or REJECTED. The exact status depends on whether Gemini classifies the severity as WARNING or CRITICAL.
The aiAnalysis field should explain the specific bleeding risk from combining an anticoagulant (Warfarin) with two drugs that also affect blood clotting (Aspirin and Ibuprofen).
Compare the two responses side by side. The safe prescription got APPROVED with no human involvement. The dangerous prescription was caught and escalated automatically.
How does the decision logic work?
Your PrescriptionService checks two fields from the AI report. If autoApproved is true, the prescription is set to APPROVED. If the severity is CRITICAL, it becomes REJECTED. Everything else lands on FLAGGED for pharmacist review.
Both prescriptions got the same status?
AI responses can vary. If the dangerous prescription also shows APPROVED, try submitting it again. Gemini occasionally misclassifies on the first attempt, but the prompt is tuned to err on the side of caution.
Why are both of my prescriptions getting the same status from the AI?
Check the pharmacist dashboard
In a real pharmacy, flagged prescriptions need a human pharmacist to review them before they can be dispensed. The /api/prescriptions/flagged endpoint acts as a simple pharmacist dashboard, returning only the prescriptions that need attention.
- Query the flagged prescriptions endpoint by running this command:
curl http://localhost:8080/api/prescriptions/flagged
You should see a JSON array containing the Jane Smith prescription. The John Doe prescription does not appear here because it was auto-approved.
Why a separate flagged endpoint?
In a full MediSynk system, this endpoint would feed an admin panel where pharmacists review and approve or reject flagged prescriptions. Separating flagged prescriptions from the full list means the pharmacist dashboard only shows the work that needs their attention.
- Verify both prescriptions are saved by querying all prescriptions:
curl http://localhost:8080/api/prescriptions
You should see both prescriptions in the response. One is APPROVED and the other is FLAGGED or REJECTED.
- Stop the application by pressing Ctrl+C in the terminal where it is running.
Flagged endpoint returning an empty array?
If Gemini classified the dangerous prescription as CRITICAL instead of WARNING, the prescription's status would be REJECTED instead of FLAGGED. The /flagged endpoint only returns prescriptions with FLAGGED status. Try querying /api/prescriptions instead to see all prescriptions and their statuses.
Why is my flagged prescriptions endpoint returning empty results?
Secret mission
Medication Expiry Monitoring
Your prescription engine is smart, but it only reacts to incoming prescriptions. Real pharmacies also need to proactively monitor their inventory. Can you add AI-powered expiry date tracking that prioritizes which near-expiry medications need urgent attention?
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with a free-tier Gemini API key, so there are no ongoing cloud costs.
Resources you used:
- Spring Boot project folder (medisynk).
- MySQL medisynk database with medications, prescriptions, and prescription_items tables.
- GEMINI_API_KEY environment variable.
- Google Gemini API key from Google AI Studio.
Keep everything running
No action needed. Choose this if you are still actively building on MediSynk or plan to extend it with the Secret Mission.
- Your MySQL medisynk database stays in place with your seeded medications and any prescriptions you submitted.
- Your project folder and Gemini API key remain ready for use.
- To restart the app later, set the GEMINI_API_KEY environment variable again and run mvnw.cmd spring-boot:run from the project folder.
Pause - I'll come back to this later
Shut down the running Spring Boot process to free up memory. Your project files and database stay intact.
- Stop the Spring Boot application by pressing Ctrl+C in the terminal where it is running.
- Your MySQL database and project files will remain on your machine.
- When you come back, set the GEMINI_API_KEY environment variable again in a new PowerShell session and restart the app.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Stop the Spring Boot application by pressing Ctrl+C in the terminal.
- Drop the MySQL database by logging into MySQL and running DROP DATABASE medisynk;.
- Delete the medisynk project folder from your machine.
- Optionally, revoke or delete your Gemini API key from Google AI Studio if you no longer need it.
Mission accomplished
That's a Wrap!
Nice work. You just built MediSynk's AI Prescription Processing Engine. This is the intelligent core of a smart pharmacy system that analyzes prescriptions, detects dangerous drug interactions, and makes automated approval decisions.
You've learned how to:
- Build a production-grade Spring Boot REST API with proper layered architecture (Controller, Service, Repository) and testable endpoints for medication management and prescription processing.
- Design a relational database with JPA entity relationships and MySQL, including Medication, Prescription, and PrescriptionItem entities with seed data for realistic testing.
- Integrate Google Gemini via Spring AI's ChatClient API with structured output conversion to build an automated prescription workflow that approves safe prescriptions and escalates dangerous ones.
- Secret Mission: Added AI-powered medication expiry monitoring with prioritized reorder recommendations.
Ready to quiz yourself?