Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.
Lesson 1 - Setup Pet Clinic App. Deployment using Docker.
Lesson 2 - Create, Read, Update, Delete (CRUD) functionality.
Lesson 3 - Homepage with content from the database. Custom not found and error pages.
Lesson 4 - Users, roles, and permissions setup from Java 2. User Registration.
Lesson 5 - Login and logout. Cookies. User permission access.
Lesson 6 - Edit and delete user profile. Password reset.
Lesson 7 - Pagination. Filtering and limiting records by category.
Lesson 8 - Web Sockets.
Lesson 9 - Internationalization. Date and currency formatting.
Lesson 10 - Email and SMS messaging. Failed login attempts.
Lesson 11 - Shopping Cart. Payment processing.
On the left menu, scroll down to Settings and click Custom domains.
Click + Add custom domain.
Select a Managed Certificate.
Type your registered domain name. I prefer adding a "www." subdomain.
Select A record for the Hostname record type.
Do not click "Add" yet. Look at the box that appears below. It will show you two critical values you need to copy:
A record IP address (A record or CNAME)
Verification ID (TXT, a long string of random characters)
Log in to SiteGround -> Services -> Domains for your domain.
Find the domain you want to use.
Go to Manage Domain -> DNS Zone Editor. Add two records.
The "Proof of Ownership" (TXT)
Name: If using the root domain (domain.com), enter: asuid
(www.domain.com), enter: asuid.www
Value: Paste the Verification ID from Azure.
The Traffic Route (A Record)
Name: If using the root domain, leave it Empty (or type @).
If using a subdomain (www), type www
Value: Paste the IP address from Azure.
www.[domain]. to [webapp].azurewebsites.net. (include the periods at the end.Click the Validate button.
It might take 1–5 minutes for SiteGround's changes to propagate. If it fails, wait a few minutes and try clicking Validate again.
Once the checks turn green, click Add.
Click on the "Add Binding" option.
Select the existing certificate.
Click Add.
Azure will now issue a free SSL certificate for your SiteGround domain.
Log in to SiteGround -> Websites.
Visit the URL in your browser. Entering the domain name without www should redirect to www.[domain]
A secure lock icon will appear when you visit the domain in the browser shortly.
aws --version
On the left menu, click Users.
Click the orange Create user button.
User details:
User name: petclinic-deployer (or whatever you prefer).
Leave the "Provide user access to the AWS Management Console" box unchecked. (This user is for the CLI only, not for clicking around the website).
Click Next.
We are going to attach two specific "Managed Policies." This gives the user power over only the tools you are currently using.
Permission options: Select Attach policies directly.
Permissions policies: In the search box, type and check the box for these two policies:
AmazonEC2ContainerRegistryPowerUser (This allows reading/writing images to the registry, but prevents deleting the registry itself.)
AWSAppRunnerFullAccess (This allows the user to create and manage the App Runner service.)
Click Next.
Review and click Create user.
Now that the user exists, you need to generate the "password" (Keys) for your terminal.
You should be back on the "Users" list. Click on the name of the user you just created (petclinic-deployer).
Click the Security credentials tab.
Scroll down to the Access keys section and click Create access key.
For the UseCase , select Command Line Interface (CLI).
Scroll down, check the "I understand..." confirmation box, and click Next.
(Optional) Set a description tag like "Laptop CLI", then click Create access key.
You will see an Access Key ID (starts with AKIA...) and a Secret Access Key (a long string of random characters).
Download the .csv file.
This is the only time AWS will ever show you the Secret Key. If you close this window without saving it, you have to delete the key and start over.
Click Create repository.
Give the repository a name, like petclinic.
Click Create.
Click the View push commands button in the top right.
AWS provides 4 commands you need to run. We'll do that in a later slide.
Go back to your terminal and run this command:
aws configure
It will ask you four questions. Copy/paste the answers from the CSV file you downloaded.
AWS Access Key ID: [Paste the AKIA... string]
AWS Secret Access Key: [Paste the secret string]
Default region name: Look at your ECR push command from before. If it said ecr.us-east-1.amazonaws.com, type us-east-1. If it said us-east-2 (Ohio) or us-west-2 (Oregon), match that.
Default output format: json
Run this command. If it replies with an "Arn" that ends in user/project-name-deployer, you are authenticated safely.
aws sts get-caller-identity
Command 1: Logs your local Docker client into AWS.
Command 2: Builds the image (you can skip this since you already built it).
Command 3: Tags your existing image.
Command 4: Pushes the image to AWS.
Before running commands 3 and4, run this command and use the name of your image.
docker images
After running command 3, run the docker images command again to verify that both images share the same ID.
After running command 4, go back to the AWS Managment Console, close the push commands modal, and click the copy URI button.
Click Create an App Runner service.
Select "Container registry" for the repository type.
Select "Amazon ECR" as the provider (the Private one, not Public).
Paste your container image URI or click the Browse button to select your image repository and tag.
Select "Create new service role". It will likely suggest a default name like AppRunnerECRAccessRole. You can leave this as is.
Click Next.
1 vCPU and 2 GB Memory is a safe choice for Spring Boot. It balances cost with the assurance that your app will actually start up without crashing or timing out. Read more on the next slides.
Environment variables: Click "Add environment variable" for each of your secrets:
MYSQL_URL : jdbc:mysql://...
MYSQL_USER : ...
MYSQL_PASS : ...
Enter "8080" for the port number for Spring Boot projects.
It will take several minutes to provision the infrastructure, pull your image, and go live. Be patient.
Think of this as the "horsepower" of your engine.
It determines how fast your code executes, and especially for Spring Boot, it determines how fast your app starts.
0.25 vCPU: Too weak. Spring Boot does a lot of complex work immediately when it turns on (scanning classes, building beans). If the CPU is this small, the startup might take so long that AWS thinks the app is "frozen" and kills it before it finishes loading.
1 vCPU: The Sweet Spot. This provides enough power to start the application quickly (usually under 30 seconds) and handle normal web traffic smoothly.
2 or 4 vCPU: Overkill. Unless you have hundreds of users hitting this specific demo app simultaneously, you are just paying for power you won't use.
Think of this as the "desk space" your app has to work on.
The Java Virtual Machine (JVM) heap requires a baseline amount of memory just to exist.
2 GB: Recommended. This gives the Java Virtual Machine (JVM) plenty of room.
3 GB / 4 GB: Unnecessary for this specific app.
You could try 1 GB if available, but Java apps in containers commonly have "Out of Memory" errors. If the app tries to grab 1.1 GB of RAM and the limit is 1 GB, the container vanishes instantly. 2 GB buys you safety.
AWS App Runner charges you based on these settings per second.
You pay for the memory (~$0.007/GB-hour) while the app is idle.
You pay for the vCPU only when the app is actively processing a request or starting up.
By choosing 1 vCPU / 2 GB, you are choosing a configuration that costs roughly $0.015 - $0.02 per hour (approx. $11-15/month) if left running 24/7.
Click the "Default domain" awsapprunner.com URL to test your app.
View the "Logs" tab in the App Runner dashboard.
Look for "Application logs" to see your Spring Boot startup output.
Unlike Azure's live stream command, AWS logs here might have a slight delay.
You must remember to "Pause" or "Delete" the service when you are done testing.
To Stop Billing, go to the App Runner service list, select your service, and click Actions -> Pause. This keeps your configuration but stops the hourly charge.
To Destroy, click Actions -> Delete.
Log in to SiteGround -> Websites -> Site Tools for your domain.
Go to Domain -> DNS Zone Editor. Add two records.
Delete any existing CNAME records.
Configure certificate validation. Create the two given CNAME records.
Name: Enter everything provided except .domain.com.
Value: Paste the values from AWS.
Configure DNS target. The the one given CNAME record.
Name: If using the root domain, leave it Empty (or type @).
If using a subdomain (www), type www
Value: Paste the value from AWS.
Wait for status to become 'Active'. It can take 24-48 hours after adding the records for the status to change.
AWSwill issue a free SSL certificate for your SiteGround domain.
A secure lock icon will appear when you visit the domain.
Follow the steps from before to redirect the domain to www
Step 1: Identify your entities
Identify your system entities, such as users, products, and orders
Step 2: Define attributes and data types for each entity
I added my entities and attributes to the downloadable Microsoft Excel file on Talon.
Access the file using Google Drive if you don't have Microsoft Excel.
I used the Event Management System ER Diagram on this website for inspiration.
Complete the Week 2 Assignment. Wait for feedback before starting the Week 3 Assignment.
Fill in the application form to apply for free student access.
When asked, "What will you use Miro for?" say something like "Class Web Design Projects."
ERD (Entity Relationship Diagram) shape packs are available for educational plans.
Create a new Miro board, or open an ER diagram template, or open one of the many examples to start with a structured layout, or launch the Mermaid integration to generate your diagram from text.
Step 3: Map relationships between entities
Step 4: Add cardinality notations
Use connection lines to define relationships between entities, such as one-to-one, one-to-many, or many-to-many.
I prompted Miro's AI with this:
I need the following tables: users, user_roles, roles, role_permissions, permissions, schools, locations, leagues, events, teams, team_users, waivers, user_waivers, messages, message_reactions, subscription, school_subscriptions, fee_payments, matches, standings. Users and roles are joined through user_roles. Roles and permissions are joined though role_permissions. Locations are joined to schools. Team_users are joined to teams. Teams are joined to leagues. Events are joined to leagues. Leagues are joined to schools and locations. User_waivers are joined to users and waivers. Messages are joined to users. Message_reactions are joined to messages. School_subscriptions are joined to schools and subscriptions. Fee_payments are joined to team_users. Matches are joined to events. Standings are joined to leagues and teams.
Step 5: Refine and validate your diagram
Rearrange, duplicate, or remove items as needed.
Step 6: Document and share
When your ER diagram is ready, invite teammates to review, comment, or co-edit in real time. Record a Talktrack to walk stakeholders through your logic asynchronously and keep everyone on the same page. When it’s time to distribute, export your diagram as an image or PDF.
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
nickname VARCHAR(50),
nickname_is_flagged TINYINT DEFAULT 0,
email VARCHAR(255) NOT NULL,
public_email TINYINT DEFAULT 0,
phone VARCHAR(255),
public_phone TINYINT DEFAULT 0,
password_hash VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME,
-- Performance: Unique index for fast login
UNIQUE INDEX idx_users_email (email),
-- Performance: Index for searching people by name
INDEX idx_users_name (last_name, first_name)
);DROP TABLE IF EXISTS schools;
CREATE TABLE schools (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
domain VARCHAR(255) NOT NULL,
status_id ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Integrity: No two schools can share the same domain
UNIQUE INDEX idx_schools_domain (domain)
);
DROP TABLE IF EXISTS locations;
CREATE TABLE locations (
id INT AUTO_INCREMENT PRIMARY KEY,
school_id INT NOT NULL,
parent_location_id INT NULL, -- Self-referencing FK (e.g., Court 1 inside Gym A)
name VARCHAR(255) NOT NULL,
description TEXT,
address VARCHAR(255),
latitude DECIMAL(8,4),
longitude DECIMAL(8,4),
status_id ENUM('draft', 'active', 'closed', 'coming_soon') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_locations_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
CONSTRAINT fk_locations_parent FOREIGN KEY (parent_location_id) REFERENCES locations(id) ON DELETE SET NULL
);Unlike JSP, that often required custom tags, Thymeleaf uses Natural Templating.
This means the template files are valid HTML files that can be opened and viewed in a browser even without the server running.
Thymeleaf works by looking for special attributes (prefixed with th:) in your HTML tags and replacing the static content with dynamic data from your Java application.
fragments/layout.html is the most critical file for understanding the UI structure. It acts as the "skeleton" for the entire application.
th:fragment="layout (template, menu)": This defines the file as a reusable layout that accepts two arguments: the specific page content (template) and the active menu item (menu).
Structure: It contains the standard HTML head (CSS/JS imports) and the navbar (navigation menu) that appears on every page.
Dynamic Insertion: The line <th:block th:insert="${template}" /> is the placeholder where the specific content of other pages (like the Welcome page or Owner form) will be injected.
The following pages define the specific content for a single view.
They do not contain the full HTML structure (like <html>, <head>, or <body> setup) themselves. Instead, they "wrap" themselves in the layout.
welcome.html Layout implementation:
It uses th:replace="~{fragments/layout :: layout (~{::body},'home')}". This tells Thymeleaf: "Take the layout fragment from fragments/layout.html, pass in my <body> tag as the content, and highlight the 'home' menu item.".
welcome.html Internationalization:
th:text="#{welcome}" replaces the text "Welcome" with a value from your messages.properties file (in resources/messages/), supporting multiple languages.
The .html pages define the specific content for a single view.
They do not contain the full HTML structure (like <html>, <head>, or <body> setup) themselves. Instead, they "wrap" themselves in the layout.
welcome.html Layout implementation:
It uses th:replace="~{fragments/layout :: layout (~{::body},'home')}". This tells Thymeleaf: "Take the layout fragment from fragments/layout.html, pass in my <body> tag as the content, and highlight the 'home' menu item.".
welcome.html Internationalization:
th:text="#{welcome}" replaces the text "Welcome" with a value from your messages.properties file (in resources/messages/), supporting multiple languages.
The messages.properties files are part of the Internationalization (i18n) system, which allows the application to be displayed in different languages without changing the code.
These files act as a dictionary that maps a "key" (used in your HTML) to a specific text string (displayed to the user).
messages/messages.properties: This is the default file. It contains the English text. For example, the key welcome is mapped to "Welcome".
messages/messages_es.properties: This is the Spanish language file (indicated by the _es suffix). It maps the same keys to Spanish text. For example, the same welcome key is mapped to "Bienvenido".
To switch to Spanish: Simply add ?lang=es to the end of your URL in the browser.
Default: http://localhost:8080/ (Displays English)
Spanish: http://localhost:8080/?lang=es (Displays Spanish)
The logic for switching languages is defined in src/main/java/org/springframework/samples/petclinic
/system/WebConfiguration.java.
This configuration sets up a LocaleChangeInterceptor with the parameter name "lang". This means the application is watching the URL for that specific query parameter.
This configuration uses a SessionLocaleResolver. Once you switch languages, the application will remember your choice for your entire browsing session (until you close the browser).
owners/createOrUpdateOwnerForm.html Form Binding:
The attribute th:object="${owner}" binds this HTML form to the Owner Java object passed from the Controller.
Instead of writing raw <input> tags, it uses th:replace="~{fragments/inputField :: input ...}". This calls a reusable fragment (defining labels, error handling, and styling) to keep the form code clean and consistent.
Key Thymeleaf Attributes You Will See
th:text: Replaces the body of the tag with a string or variable.
th:href: Creates dynamic URLs (e.g., @{/resources/css/petclinic.css} ensures the correct path context).
th:each: Loops over a list (used in vetList.html to generate table rows).
th:if / th:unless: Conditionally renders elements.
th:replace / th:insert: Injects code from one template into another.
src/main/scss/ contains the source code for your application's styling. It uses Sass (Syntactically Awesome Style Sheets), which is a "pre-processor" language that adds programming features like variables and imports to standard CSS.
petclinic.scss: This is the "master" file that bundles the other files together. It imports the Bootstrap framework and defines the application's color palette (e.g., $spring-green)
header.scss: Contains specific styles for the navigation bar and the Spring logo branding.
typography.scss: Defines the custom fonts used in the application ("Varela Round" and "Montserrat").
responsive.scss: Handles "media queries" to ensure the site looks good on mobile devices (e.g., adjusting the navbar).
src/main/resources/static/resources is the "public" folder. Anything inside static is served directly to the web browser. The browser cannot read SCSS files, so it reads the standard CSS and assets found here.
css/petclinic.css: This is the compiled result. The build process takes all the files from the scss folder, merges them with Bootstrap code, and outputs this single, standard CSS file that the browser understands.
DO NOT edit this file. Only edit the .scss files.
images/ & fonts/: These are static assets (logos, icons, font files) referenced by the CSS. For example, header.scss refers to ../images/spring-logo-dataflow.png, which physically exists in this static images folder.
The connection between Sass and CSS happens in your pom.xml file. It includes a plugin called libsass-maven-plugin.
When you run the application, this plugin:
Reads the files in src/main/scss/.
Combines them with the Bootstrap library (downloaded via WebJars).
"Compiles" them into standard CSS.
Saves the result to resources/css/petclinic.css.
src/main/scss/. If you edit the file in static/resources/css/ directly, your changes will be overwritten the next time the project is built.
owner package manages the "Pets" and "Visits" features of the application. It follows a standard Spring Boot layered architecture: Domain > Repository > Controller.Owner.java: The root entity representing a pet owner. This class extends the Person class from the model package.
Pet.java: Represents a specific animal belonging to an owner. This class extends the NamedEntity class from the model package.
Note how both Person and NamedEntity extend BaseEntity. This class defines the id field.
Note how the owners database table uses id, not owner_id.
PetType.java: A simple lookup entity defining the species (e.g., "Cat", "Dog", "Hamster", etc.).
Visit.java: Represents a medical appointment. It contains a date (defaulting to "now") and a description.
Owner.java:
Relationships: Has a One-to-Many relationship with Pet. It manages this list with methods like addPet() and getPet().
Validation: Includes constraints like @Pattern(regexp = "\\d{10}") to ensure telephone numbers are exactly 10 digits.
Pet.java:
Relationships:
Visit (a pet can have multiple doctor visits).Many-to-One with PetType (a pet belongs to one species).
Logic: It automatically sorts visits by date using @OrderBy("date ASC").
OwnerRepository.java: The primary way to access owner data.
Key Feature: It extends JpaRepository and adds a custom search method: findByLastNameStartingWith, which supports the "Find Owner" search bar feature.
PetTypeRepository.java: Provides a list of all valid animal types (findPetTypes), ordered by name, which is used to populate dropdown menus in the UI.
OwnerController.java: Manages the high-level Owner operations.
Routes: Handles listing owners (/owners), searching (/owners/find), and creating/editing owner profiles (/owners/new, /owners/{ownerId}/edit).
Note the use of @GetMapping for GET requests and @PostMapping for POST requests.
One bug I discovered is
http://localhost:8080/owners/1 (displays data for owner #1)
http://localhost:8080/owners/1/ (with slash at the end, displays an error)
I could make an Issue and Pull Request if I wanted to contribute
PetController.java: Manages the "sub-resource" of adding or editing a pet for a specific owner.
Routes: It operates under the /owners/{ownerId} path. For example, creating a new pet is handled at /owners/{ownerId}/pets/new.
VisitController.java: Manages the creation of new visits.
Routes: This is deeply nested: /owners/{ownerId}/pets/{petId}/visits/new. It ensures that before a visit is created, the system validates that the Pet and Owner actually exist.
PetValidator.java: A custom validator that ensures a Pet has a name, a birthdate, and a type. It is manually applied in the PetController.
PetTypeFormatter.java: Helps Spring MVC convert the string "Cat" from a web form into the actual PetType Java object, and vice versa.
It is possible to replace Thymeleaf with a frontend JavaScript framework like React, Angular, or Vue.
This involves shifting your application from a Server-Side Rendered (SSR) architecture to a Client-Side Rendered (CSR) or Single Page Application (SPA) architecture.
Here is how that transition works:
Thymeleaf: The Spring Boot server generates the full HTML page. It merges the data (e.g., a list of Vets) with the template (vetList.html) and sends the finished HTML to the browser.
React/Angular/Vue: The Spring Boot server becomes a REST API. It sends only the raw data (usually in JSON format). The JavaScript framework running in the browser receives that JSON and builds the HTML dynamically.
A benefit of the layered architecture (Controller > Service > Repository) is that you do not need to change your Database or Repository layers—you only need to modify the Controller layer.
If you look at VetController.java, the application already has an endpoint ready for a JavaScript framework to use:
// This method is for Thymeleaf (Returns a View)
@GetMapping("/vets.html")
public String showVetList(...) { ... }
// This method is for External Clients/JS Frameworks (Returns JSON/XML)
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
Vets vets = new Vets();
vets.getVetList().addAll(this.vetRepository.findAll());
return vets;
}The @ResponseBody annotation tells Spring: "Do not look for a Thymeleaf template. Just take this Java object, convert it to JSON, and send it back."
A React or Angular app would make a fetch('/vets') call to this URL, receive the list of doctors, and render the table itself.
To fully replace Thymeleaf, you would:
Update Controllers: Change your @Controller classes to @RestController (which automatically applies @ResponseBody to every method).
Return Data, Not Strings: Instead of returning strings like "owners/createOrUpdateOwnerForm", your methods would return Owner objects or ResponseEntity objects.
Delete Templates: You would eventually delete the src/main/resources/templates folder since the Java app no longer generates HTML.
Frontend Build: You would build your React/Angular app separately. You can then either run it on a separate server (like Node.js) that talks to your Spring Boot API, or package the built JavaScript files into the Spring Boot static folder to serve them together.
the src/test folder contains the automated tests that ensure the application works correctly. They are organized to test different "layers" of the application, from simple logic to full database integrations.
These files test start up the full Spring Boot application context to verify that all components work together.
PetClinicIntegrationTests.java: The main integration test suite that checks standard application flows.
MySqlIntegrationTests.java and PostgresIntegrationTests.java: These are specialized tests that verify the application works correctly when connected to real MySQL or PostgreSQL databases (often using Testcontainers) instead of the default in-memory H2 database.
Located in subpackages like owner and vet, these tests use @WebMvcTest. They focus only on the URL routing and HTML generation. They "mock" (fake) the service layer so they can test the UI without needing a real database.
OwnerControllerTests.java: Verifies that URLs like /owners/new return the correct view and that form submissions work.
These test individual classes in isolation.
PetValidatorTests.java: Checks specifically that the PetValidator correctly identifies invalid inputs (like a missing name).
VetTests.java: Tests the internal logic of the Vet entity, such as serialization.
Serialization is the process of converting an object's state into a sequence of bytes, which can then be saved to a file, stored in a database, or transmitted over a network.
The reverse process, converting the byte stream back into a live object, is called deserialization.
PetTypeFormatterTests.java: Ensures the string-to-object conversion for Pet Types functions as expected.
CrashControllerTests.java: specifically tests that the "Error" page triggers correctly.
I18nPropertiesSyncTest.java: A quality assurance test that ensures your translation files (like Spanish and English) stay in sync and don't have missing keys.
EntityUtils.java: Not a test itself, but a helper class used by other tests to look up objects in collections.
This is the Maven configuration file. It tells Java exactly which third-party libraries (dependencies) your project needs to run.
Parent POM: You will see a <parent> tag for spring-boot-starter-parent. This is the "magic" that manages versions for you. You don't need to specify the version for every single library because the parent knows which versions work together.
Starters: Notice dependencies like spring-boot-starter-web, spring-boot-starter-data-jpa, and spring-boot-starter-thymeleaf. These are pre-packaged bundles. For example, starter-web automatically gives you Tomcat (server), Spring MVC (framework), and Jackson (JSON parser) in one go.
Java Version: It explicitly sets the Java version to 17 (<java.version>17</java.version>).
Located in src/main/resources/, this is where you configure the application's behavior without changing Java code.
Database Settings: The line database=h2 tells the app to use the H2 in-memory database by default. This is why you don't need to install MySQL to run the demo; the database is created in RAM when you start the app and destroyed when you stop it.
Database Initialization: It points to schema.sql (to create tables) and data.sql (to load dummy data).
Logging: You can control how much info appears in the console. For example, logging.level.org.springframework=INFO.
This is the file with the public static void main method that launches the app.
@SpringBootApplication: This single annotation actually triggers three powerful features:
Configuration: It marks the class as a source of bean definitions.
EnableAutoConfiguration: It tells Spring Boot to configure your app based on the libraries in your pom.xml
ComponentScan: It tells Spring to scan the current package (and all sub-packages) for your Controllers, Services, and Repositories.
While application.properties is good for simple key-value pairs, complex configuration is done in Java classes marked with @Configuration.
@EnableCaching: This annotation turns on Spring's caching ability.
@Bean: The method petclinicCacheConfigurationCustomizer() is marked with @Bean. This tells Spring: "Execute this method and manage the object it returns.". It is used here to set up a specific cache for "vets" so the database doesn't have to be queried every time the user refreshes the veterinarian list.
The term "Bean" is used in two distinct ways in this project. It can be confusing because they share the same name, but they serve different purposes.
In the comments of files like Vet.java and Owner.java, you will see them described as a "Simple JavaBean domain object".
A standard JavaBean is simply a Java class that follows a specific set of naming conventions so other tools can easily use it.
Private Properties: Data fields are hidden (e.g., private String address; in Owner.java).
Public Getters/Setters: The data is accessed via public methods named get... and set... (e.g., getAddress() and setAddress()).
In your project: Classes like Vet, Owner, Pet, and Visit are standard JavaBeans. They are just containers for your data.
No-Argument Constructor: It usually has a default constructor so frameworks can create an empty instance of it easily.
JavaBean is a pattern for writing data classes (like Vet or Owner).
In CacheConfiguration.java and WebConfiguration.java, you saw the @Bean annotation.
A Spring Bean is an object that is created, configured, and managed by the Spring Framework itself (the "Spring Container").
Managed Lifecycle: You do not create these objects with new (e.g., new VetController()). Instead, Spring creates them for you at startup.
Dependency Injection: Spring "injects" these beans into other beans that need them. For example, VetRepository is a Spring Bean that is automatically injected into the VetController.
Spring Bean is a specific object living in the Spring system (like VetController or the CacheManager).
GitHub Actions works as your "robot butler." Instead of you manually running gradle build, docker build, and docker push on your laptop, GitHub does it automatically every time you push code.
Here is the workflow:
You push code to your GitHub repository.
GitHub Actions wakes up, spins up a temporary server (runner).
It builds your jar using Gradle.
It logs in to Docker Hub (using secrets you provide).
It builds and pushes your Docker image.
You should never put your Docker Hub password directly in the file. You use GitHub Secrets.
Go to your GitHub repository.
Click Settings -> Secrets and variables -> Actions.
Click New repository secret.
Add these two:
Name: DOCKERHUB_USERNAME
Value: your_actual_username
Name: DOCKERHUB_TOKEN
Value: (Go to Docker Hub -> Account Settings -> Security -> New Access Token. Use this token instead of your real password.)
In your project, create this directory structure and file: .github/workflows/deploy.yml
Paste this content in. I have customized it for Gradle and Java 17.
name: Build and Push Docker Image
on:
push:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
# 1. Download your code
- name: Checkout code
uses: actions/checkout@v4
# 2. Set up Java 17 (Same as your local environment)
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin' # or 'microsoft' if you prefer
# 3. Build the JAR file with Gradle
# We skip tests here to speed it up, but you can remove '-x test' to be safer
- name: Build with Gradle
run: ./gradlew clean bootJar -x test -x jar
# 4. Set up Docker Buildx (required for modern Docker builds)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 5. Log in to Docker Hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 6. Build and Push the Docker image
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/petclinic:latestCommit this file and push it to GitHub.
git add .
git commit -m "Add GitHub Action workflow"
git push
Go to the "Actions" tab in your GitHub repository. You will see the workflow running. When it turns green, check Docker Hub—your image will have been freshly updated!
Right now, GitHub pushes the image, but Azure doesn't know about it yet. You have two choices:
Continuous Deployment (CD) in Azure:
Go to your Azure Container App in the portal.
Under Application -> Revision management (or Continuous deployment), you can enable a setting that says "Create a new revision when a new image is pushed."
Note: This often requires a webhook setup.
Add an Azure Step to the YAML:
You can add a final step to the GitHub Action that runs az containerapp update to tell Azure to pull the new image immediately.
Would you like the code snippet to force Azure to update at the end of this workflow?
DROP TABLE IF EXISTS leagues;
CREATE TABLE leagues (
id INT AUTO_INCREMENT PRIMARY KEY,
school_id INT NOT NULL,
location_id INT, -- Default location
user_id INT, -- League Manager/Contact
name VARCHAR(255) NOT NULL,
description TEXT,
registration_start DATETIME,
registration_end DATETIME,
league_start DATETIME,
league_end DATETIME,
is_public TINYINT DEFAULT 1,
type ENUM('male', 'female', 'coed') NOT NULL,
capacity INT,
capacity_type ENUM('team', 'individual') NOT NULL,
fee DECIMAL(6,2),
status_id ENUM('draft', 'active', 'inactive', 'postponed', 'cancelled', 'past') DEFAULT 'draft',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_leagues_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
CONSTRAINT fk_leagues_location FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE SET NULL,
CONSTRAINT fk_leagues_manager FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
-- Performance: Quickly find active leagues for a specific school
INDEX idx_leagues_school_status (school_id, status_id)
);
DROP TABLE IF EXISTS teams;
CREATE TABLE teams (
id INT AUTO_INCREMENT PRIMARY KEY,
league_id INT NOT NULL,
captain_user_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
logo_url VARCHAR(255),
status_id ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_teams_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
CONSTRAINT fk_teams_captain FOREIGN KEY (captain_user_id) REFERENCES users(id) ON DELETE CASCADE,
-- Performance: List all teams in a league
INDEX idx_teams_league (league_id)
);
DROP TABLE IF EXISTS team_users;
CREATE TABLE team_users (
id INT AUTO_INCREMENT PRIMARY KEY,
team_id INT NOT NULL,
user_id INT NOT NULL,
role ENUM('member', 'captain') DEFAULT 'member',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_team_users_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
CONSTRAINT fk_team_users_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
-- Integrity: A user cannot join the same team twice
UNIQUE INDEX idx_team_users_unique (team_id, user_id)
);DROP TABLE IF EXISTS events;
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
league_id INT NOT NULL,
location_id INT,
user_id INT, -- Event organizer/referee
name VARCHAR(255) NOT NULL,
description TEXT,
event_start DATETIME,
event_end DATETIME,
status_id ENUM('draft', 'active', 'ongoing', 'postponed', 'cancelled', 'final') DEFAULT 'draft',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_events_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
CONSTRAINT fk_events_location FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE SET NULL,
CONSTRAINT fk_events_contact FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
-- Integrity & Performance: Check for double-bookings at a location
INDEX idx_events_location_time (location_id, event_start)
);
DROP TABLE IF EXISTS matches;
CREATE TABLE matches (
id INT AUTO_INCREMENT PRIMARY KEY,
event_id INT NOT NULL,
home_team_id INT,
away_team_id INT,
winner_team_id INT,
home_score INT DEFAULT 0,
away_score INT DEFAULT 0,
status ENUM('scheduled', 'in_progress', 'final', 'forfeit') DEFAULT 'scheduled',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_matches_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
CONSTRAINT fk_matches_home FOREIGN KEY (home_team_id) REFERENCES teams(id) ON DELETE SET NULL,
CONSTRAINT fk_matches_away FOREIGN KEY (away_team_id) REFERENCES teams(id) ON DELETE SET NULL,
CONSTRAINT fk_matches_winner FOREIGN KEY (winner_team_id) REFERENCES teams(id) ON DELETE SET NULL,
-- Performance: Essential for calculating standings (W/L records)
INDEX idx_matches_home (home_team_id),
INDEX idx_matches_away (away_team_id)
);DROP TABLE IF EXISTS messages;
CREATE TABLE messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id_from INT NOT NULL,
user_id_to INT,
league_id INT,
event_id INT,
parent_message_id INT, -- For threaded replies
message VARCHAR(255) NOT NULL,
is_flagged TINYINT DEFAULT 0,
status_id ENUM('draft', 'active', 'hidden') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_messages_from FOREIGN KEY (user_id_from) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_messages_to FOREIGN KEY (user_id_to) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_messages_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
CONSTRAINT fk_messages_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
CONSTRAINT fk_messages_parent FOREIGN KEY (parent_message_id) REFERENCES messages(id) ON DELETE CASCADE,
-- Performance: Quickly load chat history for a league or event
INDEX idx_messages_context (league_id, event_id)
);
DROP TABLE IF EXISTS message_reactions;
CREATE TABLE message_reactions (
id INT AUTO_INCREMENT PRIMARY KEY,
message_id INT NOT NULL,
user_id INT NOT NULL,
reaction ENUM('like', 'dislike', 'love', 'hug', 'sad', 'angry'),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_reactions_message FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
CONSTRAINT fk_reactions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
-- Integrity: One reaction per user per message
UNIQUE INDEX idx_reactions_unique (message_id, user_id)
);By Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.